diff --git a/urbanops-framework/urbanops-spring-boot-starter-protection/src/main/java/com/zteits/urbanops/framework/signature/core/aop/ApiSignatureAspect.java b/urbanops-framework/urbanops-spring-boot-starter-protection/src/main/java/com/zteits/urbanops/framework/signature/core/aop/ApiSignatureAspect.java index a1c30ee..d5deed2 100644 --- a/urbanops-framework/urbanops-spring-boot-starter-protection/src/main/java/com/zteits/urbanops/framework/signature/core/aop/ApiSignatureAspect.java +++ b/urbanops-framework/urbanops-spring-boot-starter-protection/src/main/java/com/zteits/urbanops/framework/signature/core/aop/ApiSignatureAspect.java @@ -37,7 +37,7 @@ public class ApiSignatureAspect { private final ApiSignatureRedisDAO signatureRedisDAO; - @Before("@annotation(signature)") + @Before("@within(signature)") public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { // 1. 验证通过,直接结束 if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { @@ -171,4 +171,4 @@ public class ApiSignatureAspect { return sortedMap; } -} \ No newline at end of file +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java index dc2fcf6..1f929e0 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java @@ -12,6 +12,7 @@ import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; import static com.zteits.urbanops.framework.common.pojo.CommonResult.success; @@ -52,4 +53,16 @@ public class AppHomepageSummaryController { public CommonResult iWorkOrderSummary(@Valid @RequestBody AppWorkOrderSummaryReqVo req) { return success(homepageSummaryService.iWorkOrderSummary(req)); } + + @PostMapping("/taskDetailsWithDistance") + @Operation(summary = "任务待办(按距离排序 + 班组进度)") + public CommonResult queryTaskDetailsWithDistance(@Valid @RequestBody AppTaskDistanceSortReqVO req) { + return success(homepageSummaryService.queryTaskDetailsWithDistance(req)); + } + + @PostMapping("/taskDetailsWithSmartSort") + @Operation(summary = "任务待办(智能排序:距离 + 个人效率加权)") + public CommonResult queryTaskDetailsWithSmartSort(@Valid @RequestBody AppTaskSmartSortReqVO req) { + return success(homepageSummaryService.queryTaskDetailsWithSmartSort(req)); + } } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskDistanceSortReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskDistanceSortReqVO.java new file mode 100644 index 0000000..4ff790a --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskDistanceSortReqVO.java @@ -0,0 +1,31 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import com.zteits.urbanops.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * app - 任务距离排序 请求 VO + * + * @author + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AppTaskDistanceSortReqVO extends PageParam { + + @Schema(description = "查询类型:1-待办,2-已办", example = "1") + @NotNull + public Integer queryType; + + @Schema(description = "当前经度", example = "116.397428") + @NotNull(message = "当前经度不能为空") + private BigDecimal currentLon; + + @Schema(description = "当前纬度", example = "39.909204") + @NotNull(message = "当前纬度不能为空") + private BigDecimal currentLat; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskSmartSortReqVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskSmartSortReqVO.java new file mode 100644 index 0000000..1c75f3f --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskSmartSortReqVO.java @@ -0,0 +1,31 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import com.zteits.urbanops.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * app - 任务智能排序 请求 VO + * + * @author + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AppTaskSmartSortReqVO extends PageParam { + + @Schema(description = "查询类型:1-待办,2-已办", example = "1") + @NotNull + private Integer queryType; + + @Schema(description = "当前经度", example = "116.397428") + @NotNull(message = "当前经度不能为空") + private BigDecimal currentLon; + + @Schema(description = "当前纬度", example = "39.909204") + @NotNull(message = "当前纬度不能为空") + private BigDecimal currentLat; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortRespVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortRespVO.java new file mode 100644 index 0000000..85777f8 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortRespVO.java @@ -0,0 +1,58 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/** + * app - 任务距离排序 单个任务响应 VO + * + * @author + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class TaskDistanceSortRespVO extends TaskDetailsRspVo { + + /** 任务类型:inspection / maintain / workorder */ + private String taskType; + + /** 任务点经度 */ + private BigDecimal taskLon; + + /** 任务点纬度 */ + private BigDecimal taskLat; + + /** 从上一点到本任务的距离(米),无 GPS 则为 null */ + private Integer distance; + + /** 从起点累计距离(米) */ + private Integer cumulativeDistance; + + /** 预估步行时间(分钟) */ + private Integer estimatedWalkingMinutes; + + /** 预估三轮车时间(分钟) */ + private Integer estimatedTricycleMinutes; + + /** 排序序号:1, 2, 3... */ + private Integer sortOrder; + + /** 任务子类型:plan_type_id 或 order_type,用于效率矩阵匹配 */ + private String taskSubType; + + /** 距离得分 [0, 1] */ + private Double distanceScore; + + /** 效率得分(已截断) */ + private Double efficiencyScore; + + /** 综合得分 */ + private Double compositeScore; + + /** 用户历史平均完成时长(分钟) */ + private BigDecimal userAvgMinutes; + + /** 班组平均完成时长(分钟) */ + private BigDecimal teamAvgMinutes; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortResultVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortResultVO.java new file mode 100644 index 0000000..26831d2 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortResultVO.java @@ -0,0 +1,32 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import lombok.Data; + +import java.util.List; + +/** + * app - 任务距离排序 顶层返回 VO + * + * @author + */ +@Data +public class TaskDistanceSortResultVO { + + /** 排序后的任务列表 */ + private List sortedTasks; + + /** 班组内进度统计 */ + private TeamProgressVO teamProgress; + + /** 预估总距离(米) */ + private Integer estimatedTotalDistance; + + /** 预估总步行时间(分钟) */ + private Integer estimatedTotalWalkingMinutes; + + /** 预估总三轮车时间(分钟) */ + private Integer estimatedTotalTricycleMinutes; + + /** 总待办任务数 */ + private Integer totalPendingTasks; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TeamProgressVO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TeamProgressVO.java new file mode 100644 index 0000000..44b31f5 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TeamProgressVO.java @@ -0,0 +1,24 @@ +package com.zteits.urbanops.module.garden.controller.app.homepage.vo; + +import lombok.Data; + +/** + * app - 班组内进度统计 VO + * + * @author + */ +@Data +public class TeamProgressVO { + + /** 当前用户今日已完成数 */ + private Integer userCompletedCount; + + /** 班组均值 */ + private Double teamAverage; + + /** 班组内最高 */ + private Integer teamMax; + + /** 班组总人数 */ + private Integer teamMemberCount; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/UserTaskEfficiencyDO.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/UserTaskEfficiencyDO.java new file mode 100644 index 0000000..4800a99 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/UserTaskEfficiencyDO.java @@ -0,0 +1,58 @@ +package com.zteits.urbanops.module.garden.dal.dataobject; + +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO; +import lombok.*; + +import java.math.BigDecimal; + +/** + * 用户任务效率预计算 DO + * + * @author + */ +@TableName("garden_user_task_efficiency") +@KeySequence("garden_user_task_efficiency_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserTaskEfficiencyDO extends BaseDO { + + @TableId + private Long id; + + /** 用户ID */ + private Long userId; + + /** 部门ID(班组) */ + private Long deptId; + + /** 任务大类:inspection / maintain / workorder */ + private String taskCategory; + + /** 任务类型(细粒度):plan_type_id 或 order_type,ALL 表示大类汇总 */ + private String taskType; + + /** 6个自然月内完成任务数 */ + private Integer completedCount; + + /** 总耗时(分钟) */ + private Long totalMinutes; + + /** 个人平均完成时长(分钟) */ + private BigDecimal avgMinutes; + + /** 班组平均完成时长(分钟) */ + private BigDecimal teamAvgMinutes; + + /** 效率比 = teamAvg / userAvg */ + private BigDecimal efficiencyRatio; + + /** 统计周期 yyyy-MM */ + private String statPeriod; +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/UserTaskEfficiencyMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/UserTaskEfficiencyMapper.java new file mode 100644 index 0000000..6c3f04d --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/UserTaskEfficiencyMapper.java @@ -0,0 +1,43 @@ +package com.zteits.urbanops.module.garden.dal.mysql; + +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX; +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 用户任务效率 Mapper + * + * @author + */ +@Mapper +public interface UserTaskEfficiencyMapper extends BaseMapperX { + + /** + * 根据用户和统计周期查询效率数据 + */ + List selectByUserIdAndPeriod(@Param("userId") Long userId, + @Param("statPeriod") String statPeriod); + + /** + * 查询最近一条有数据的统计周期 + */ + String selectLatestPeriod(@Param("userId") Long userId); + + /** + * 删除指定统计周期的数据(幂等重跑) + */ + int deleteByPeriod(@Param("statPeriod") String statPeriod); + + /** 巡查计划效率统计 */ + List> statInspectionEfficiency(@Param("beginTime") String beginTime); + + /** 养护计划效率统计 */ + List> statMaintainEfficiency(@Param("beginTime") String beginTime); + + /** 工单效率统计 */ + List> statWorkOrderEfficiency(@Param("beginTime") String beginTime); +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java index 40d10be..ad3ad9a 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java @@ -10,6 +10,7 @@ import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppWorkOrder import com.zteits.urbanops.module.garden.controller.app.homepage.vo.CommonTaskStatusVo; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo; +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDistanceSortRespVO; import com.zteits.urbanops.module.garden.dal.dataobject.departmentcost.DepartmentCostDO; import com.zteits.urbanops.module.garden.dal.dataobject.taskstatistics.TaskStatisticsDO; import org.apache.ibatis.annotations.Mapper; @@ -216,4 +217,17 @@ public interface HomepageSummaryMapper extends BaseMapperX { * @Return */ List countWorkOrder(@Param("queryType") String queryType,@Param("userId") Long userId, @Param("beginTime") String beginTime, @Param("endTime") String endTime); + + /** + * 查询今日待办任务(含 GPS 坐标),三表 UNION + */ + List queryPendingTaskDetailsWithGps(@Param("roleIds") List roleIds, + @Param("userId") Long userId, + @Param("deptId") Long deptId, + @Param("currentDate") LocalDateTime currentDate); + + /** + * 统计今日同班组(同 dept_id)成员完成任务数 + */ + List> countTeamCompletedToday(@Param("deptId") Long deptId); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/CommonConstants.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/CommonConstants.java index 0bf87a0..b878bcb 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/CommonConstants.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/CommonConstants.java @@ -26,4 +26,7 @@ public interface CommonConstants { /*积分来源 01:任务反馈*/ public static final String UNIT_INTEGRAL_SOURCE_TYPE_01 = "01"; + + /*全域督察员角色标识*/ + public static final String INSPECTOR_ROLE_KEY = "inspector"; } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/TaskEfficiencyCalculateJob.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/TaskEfficiencyCalculateJob.java new file mode 100644 index 0000000..e8364eb --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/TaskEfficiencyCalculateJob.java @@ -0,0 +1,173 @@ +package com.zteits.urbanops.module.garden.job; + +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO; +import com.zteits.urbanops.module.garden.dal.mysql.UserTaskEfficiencyMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 用户任务效率预计算定时任务 + * 每天凌晨 2:00 执行,统计最近 6 个自然月的用户任务完成效率 + * + * @author + */ +@Slf4j +@Component +public class TaskEfficiencyCalculateJob { + + @Resource + private UserTaskEfficiencyMapper userTaskEfficiencyMapper; + + public void execute() { + String statPeriod = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); + log.info("TaskEfficiencyCalculateJob start, statPeriod={}", statPeriod); + try { + // 1. 删除本月旧数据 + userTaskEfficiencyMapper.deleteByPeriod(statPeriod); + + // 2. 6个月窗口 + String beginTime = LocalDate.now().minusMonths(6).withDayOfMonth(1) + " 00:00:00"; + + // 3. 三类任务分别统计 + List all = new ArrayList<>(); + all.addAll(mapRows(userTaskEfficiencyMapper.statInspectionEfficiency(beginTime), statPeriod)); + all.addAll(mapRows(userTaskEfficiencyMapper.statMaintainEfficiency(beginTime), statPeriod)); + all.addAll(mapRows(userTaskEfficiencyMapper.statWorkOrderEfficiency(beginTime), statPeriod)); + + if (all.isEmpty()) { + log.info("TaskEfficiencyCalculateJob done, no data"); + return; + } + + // 4. 计算班组均值 + calcTeamAverage(all); + + // 5. 计算效率比 + for (UserTaskEfficiencyDO e : all) { + e.setEfficiencyRatio(calcRatio(e.getAvgMinutes(), e.getTeamAvgMinutes())); + } + + // 6. 生成大类汇总(ALL) + all.addAll(buildCategorySummary(all, statPeriod)); + + // 7. 批量写入 + LocalDateTime now = LocalDateTime.now(); + for (UserTaskEfficiencyDO e : all) { + e.setCreator("job"); + e.setUpdater("job"); + e.setCreateTime(now); + e.setUpdateTime(now); + userTaskEfficiencyMapper.insert(e); + } + + log.info("TaskEfficiencyCalculateJob done, total={}", all.size()); + } catch (Exception e) { + log.error("TaskEfficiencyCalculateJob error", e); + } + } + + private List mapRows(List> rows, String statPeriod) { + List result = new ArrayList<>(); + if (rows == null || rows.isEmpty()) return result; + for (Map row : rows) { + UserTaskEfficiencyDO e = new UserTaskEfficiencyDO(); + e.setUserId(toLong(row.get("user_id"))); + e.setDeptId(toLong(row.get("dept_id"))); + e.setTaskCategory((String) row.get("task_category")); + e.setTaskType(String.valueOf(row.get("task_type"))); + e.setCompletedCount(toInt(row.get("cnt"))); + e.setTotalMinutes(toLong(row.get("total_min"))); + e.setAvgMinutes(toDecimal(row.get("avg_min"))); + e.setStatPeriod(statPeriod); + result.add(e); + } + return result; + } + + private void calcTeamAverage(List all) { + Map> groups = new LinkedHashMap<>(); + for (UserTaskEfficiencyDO e : all) { + String key = e.getDeptId() + "_" + e.getTaskCategory() + "_" + e.getTaskType(); + groups.computeIfAbsent(key, k -> new ArrayList<>()).add(e); + } + for (List group : groups.values()) { + long totalMin = 0; + int totalCnt = 0; + for (UserTaskEfficiencyDO e : group) { + totalMin += e.getTotalMinutes() != null ? e.getTotalMinutes() : 0; + totalCnt += e.getCompletedCount() != null ? e.getCompletedCount() : 0; + } + BigDecimal teamAvg = totalCnt > 0 + ? BigDecimal.valueOf(totalMin).divide(BigDecimal.valueOf(totalCnt), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + for (UserTaskEfficiencyDO e : group) { + e.setTeamAvgMinutes(teamAvg); + } + } + } + + private BigDecimal calcRatio(BigDecimal userAvg, BigDecimal teamAvg) { + if (userAvg == null || teamAvg == null + || userAvg.compareTo(BigDecimal.ZERO) <= 0 + || teamAvg.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ONE; + } + BigDecimal ratio = teamAvg.divide(userAvg, 4, RoundingMode.HALF_UP); + if (ratio.compareTo(new BigDecimal("3.0")) > 0) return new BigDecimal("3.0"); + if (ratio.compareTo(new BigDecimal("0.3")) < 0) return new BigDecimal("0.3"); + return ratio; + } + + private List buildCategorySummary(List all, String statPeriod) { + Map> catMap = new LinkedHashMap<>(); + for (UserTaskEfficiencyDO e : all) { + String key = e.getUserId() + "_" + e.getDeptId() + "_" + e.getTaskCategory(); + catMap.computeIfAbsent(key, k -> new ArrayList<>()).add(e); + } + List summaries = new ArrayList<>(); + for (List group : catMap.values()) { + UserTaskEfficiencyDO s = new UserTaskEfficiencyDO(); + s.setUserId(group.get(0).getUserId()); + s.setDeptId(group.get(0).getDeptId()); + s.setTaskCategory(group.get(0).getTaskCategory()); + s.setTaskType("ALL"); + s.setStatPeriod(statPeriod); + long totalMin = 0; + int totalCnt = 0; + for (UserTaskEfficiencyDO e : group) { + totalMin += e.getTotalMinutes() != null ? e.getTotalMinutes() : 0; + totalCnt += e.getCompletedCount() != null ? e.getCompletedCount() : 0; + } + s.setCompletedCount(totalCnt); + s.setTotalMinutes(totalMin); + s.setAvgMinutes(totalCnt > 0 + ? BigDecimal.valueOf(totalMin).divide(BigDecimal.valueOf(totalCnt), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO); + s.setTeamAvgMinutes(group.get(0).getTeamAvgMinutes()); + s.setEfficiencyRatio(calcRatio(s.getAvgMinutes(), s.getTeamAvgMinutes())); + summaries.add(s); + } + return summaries; + } + + private Long toLong(Object o) { + if (o == null) return 0L; + return o instanceof Number ? ((Number) o).longValue() : Long.parseLong(o.toString()); + } + private Integer toInt(Object o) { + if (o == null) return 0; + return o instanceof Number ? ((Number) o).intValue() : Integer.parseInt(o.toString()); + } + private BigDecimal toDecimal(Object o) { + if (o == null) return BigDecimal.ZERO; + return o instanceof BigDecimal ? (BigDecimal) o : new BigDecimal(o.toString()); + } +} diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java index c535741..692751e 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java @@ -4,6 +4,7 @@ import com.zteits.urbanops.framework.common.pojo.PageResult; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*; import java.util.List; +import java.util.Map; /** * @Classname HomepageSummaryService @@ -48,4 +49,16 @@ public interface HomepageSummaryService { AppWorkOrderSummaryRspVo iWorkOrderSummary(AppWorkOrderSummaryReqVo req); + /** + * 任务待办(按距离排序 + 班组进度) + * + * @param req 请求参数(当前 GPS 坐标) + * @return 排序后的任务列表 + 班组进度统计 + */ + TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req); + + /** + * 任务待办(智能排序:距离 + 个人效率加权) + */ + TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req); } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java index 0a1cfea..8f94b1e 100644 --- a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java @@ -5,27 +5,38 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.esotericsoftware.minlog.Log; import com.zteits.urbanops.framework.common.pojo.PageResult; +import com.zteits.urbanops.framework.common.util.date.DateUtils; import com.zteits.urbanops.framework.common.util.date.LocalDateTimeUtils; +import com.zteits.urbanops.framework.mybatis.core.query.QueryWrapperX; import com.zteits.urbanops.framework.mybatis.core.util.MyBatisUtils; import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils; +import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO; import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*; +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO; +import com.zteits.urbanops.module.garden.enums.CommonConstants; import com.zteits.urbanops.module.garden.dal.dataobject.taskstatistics.TaskStatisticsDO; +import com.zteits.urbanops.module.garden.dal.mysql.UserTaskEfficiencyMapper; import com.zteits.urbanops.module.garden.dal.mysql.homepage.HomepageSummaryMapper; import com.zteits.urbanops.module.garden.dal.mysql.taskstatistics.TaskStatisticsMapper; +import com.zteits.urbanops.module.garden.util.GpsDistanceUtil; import com.zteits.urbanops.module.system.api.dept.DeptApi; +import com.zteits.urbanops.module.system.api.dict.DictDataApi; import com.zteits.urbanops.module.system.api.permission.RoleApi; import com.zteits.urbanops.module.system.api.user.AdminUserApi; -import com.zteits.urbanops.module.system.enums.common.CommonConstants; +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO; +import com.zteits.urbanops.module.system.enums.DictTypeConstants; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -58,6 +69,12 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ @Resource private DeptApi deptApi; + @Resource + private DictDataApi dictDataApi; + + @Resource + private UserTaskEfficiencyMapper userTaskEfficiencyMapper; + @Override public List taskCompletionSummary(AppHomePageSummaryReqVO req) { Long userId = getLoginUserId(); @@ -408,4 +425,360 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{ return days; } + + @Override + public TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req) { + // 1. 获取当前登录用户信息 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long deptId = SecurityFrameworkUtils.getDeptId(); + List roleIds = roleApi.getRoleIdsByUserId(userId); + + // 2. 根据 queryType 分流 + List sortedTasks; + if (req.getQueryType() != null && req.getQueryType() == 2) { + // ========== 已办:查询已完成任务(无 GPS 距离排序) ========== + sortedTasks = queryCompletedTasksWithoutGps(userId); + } else { + // ========== 待办:查询待办任务 + GPS 贪心最近邻排序 ========== + sortedTasks = queryPendingTasksWithGpsSort(req, userId, deptId, roleIds); + } + + // 3. 查询班组进度 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId); + + // 4. 组装返回结果 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO(); + result.setSortedTasks(sortedTasks); + result.setTeamProgress(teamProgress); + result.setTotalPendingTasks(sortedTasks.size()); + int totalDist = 0; + int totalWalkMin = 0; + int totalTriMin = 0; + for (TaskDistanceSortRespVO task : sortedTasks) { + if (task.getDistance() != null) { + totalDist += task.getDistance(); + totalWalkMin += (task.getEstimatedWalkingMinutes() != null ? task.getEstimatedWalkingMinutes() : 0); + totalTriMin += (task.getEstimatedTricycleMinutes() != null ? task.getEstimatedTricycleMinutes() : 0); + } + } + result.setEstimatedTotalDistance(totalDist); + result.setEstimatedTotalWalkingMinutes(totalWalkMin); + result.setEstimatedTotalTricycleMinutes(totalTriMin); + + return result; + } + + /** + * 已办任务查询(无 GPS 排序) + */ + private List queryCompletedTasksWithoutGps(Long userId) { + IPage page = MyBatisUtils.buildPage(new AppHomePageSummaryPageReqVO()); + // 设置大分页以获取全部已办任务 + page.setSize(Integer.MAX_VALUE); + IPage mpPage = homepageSummaryMapper.queryCompletedTaskDetails(page, userId); + List result = new ArrayList<>(); + if (mpPage != null && CollectionUtils.isNotEmpty(mpPage.getRecords())) { + int i = 0; + for (TaskDetailsRspVo vo : mpPage.getRecords()) { + TaskDistanceSortRespVO sortVo = new TaskDistanceSortRespVO(); + sortVo.setTaskName(vo.getTaskName()); + sortVo.setBusiDateTime(vo.getBusiDateTime()); + sortVo.setPressingType(vo.getPressingType()); + sortVo.setSortOrder(++i); + sortVo.setDistance(null); + sortVo.setCumulativeDistance(null); + sortVo.setEstimatedWalkingMinutes(null); + sortVo.setEstimatedTricycleMinutes(null); + result.add(sortVo); + } + } + return result; + } + + /** + * 待办任务查询 + GPS 贪心最近邻排序 + */ + private List queryPendingTasksWithGpsSort(AppTaskDistanceSortReqVO req, + Long userId, Long deptId, + List roleIds) { + // 从字典表读取速度配置 + double walkingSpeedKmh = 4.0; + double tricycleSpeedKmh = 15.0; + try { + List speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG); + if (CollectionUtils.isNotEmpty(speedConfigs)) { + for (DictDataRespDTO config : speedConfigs) { + if ("步行速度(km/h)".equals(config.getLabel())) { + walkingSpeedKmh = Double.parseDouble(config.getValue()); + } else if ("三轮车速度(km/h)".equals(config.getLabel())) { + tricycleSpeedKmh = Double.parseDouble(config.getValue()); + } + } + } + } catch (Exception e) { + Log.warn("读取出行速度字典配置失败,使用默认值", e); + } + double walkingSpeedMperMin = walkingSpeedKmh * 1000 / 60; + double tricycleSpeedMperMin = tricycleSpeedKmh * 1000 / 60; + + // 查询今日待办任务(含 GPS 坐标) + LocalDateTime now = LocalDateTime.now(); + List allTasks = homepageSummaryMapper + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, now); + + // 分离有 GPS 和无 GPS 任务 + List tasksWithGps = new ArrayList<>(); + List tasksWithoutGps = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(allTasks)) { + for (TaskDistanceSortRespVO task : allTasks) { + if (task.getTaskLat() != null && task.getTaskLon() != null) { + tasksWithGps.add(task); + } else { + tasksWithoutGps.add(task); + } + } + } + + // 贪心最近邻排序 + List sortedTasks = new ArrayList<>(); + double currentLat = req.getCurrentLat().doubleValue(); + double currentLon = req.getCurrentLon().doubleValue(); + int cumulativeDistance = 0; + int sortOrder = 0; + + List remaining = new ArrayList<>(tasksWithGps); + while (!remaining.isEmpty()) { + TaskDistanceSortRespVO nearest = null; + double minDistance = Double.MAX_VALUE; + int nearestIndex = -1; + for (int i = 0; i < remaining.size(); i++) { + TaskDistanceSortRespVO task = remaining.get(i); + double dist = GpsDistanceUtil.haversineDistance( + currentLat, currentLon, + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue()); + if (dist < minDistance) { + minDistance = dist; + nearest = task; + nearestIndex = i; + } + } + sortOrder++; + int distMeters = (int) Math.round(minDistance); + cumulativeDistance += distMeters; + nearest.setSortOrder(sortOrder); + nearest.setDistance(distMeters); + nearest.setCumulativeDistance(cumulativeDistance); + nearest.setEstimatedWalkingMinutes((int) Math.ceil(distMeters / walkingSpeedMperMin)); + nearest.setEstimatedTricycleMinutes((int) Math.ceil(distMeters / tricycleSpeedMperMin)); + sortedTasks.add(nearest); + + currentLat = nearest.getTaskLat().doubleValue(); + currentLon = nearest.getTaskLon().doubleValue(); + remaining.remove(nearestIndex); + } + + // 无 GPS 任务追加到末尾 + if (CollectionUtils.isNotEmpty(tasksWithoutGps)) { + for (TaskDistanceSortRespVO task : tasksWithoutGps) { + sortOrder++; + task.setSortOrder(sortOrder); + task.setDistance(null); + task.setCumulativeDistance(null); + task.setEstimatedWalkingMinutes(null); + task.setEstimatedTricycleMinutes(null); + sortedTasks.add(task); + } + } + return sortedTasks; + } + + /** + * 构建班组进度统计 + */ + private TeamProgressVO buildTeamProgress(Long userId, Long deptId) { + TeamProgressVO teamProgress = new TeamProgressVO(); + teamProgress.setUserCompletedCount(0); + teamProgress.setTeamAverage(0.0); + teamProgress.setTeamMax(0); + teamProgress.setTeamMemberCount(0); + try { + List> teamData = homepageSummaryMapper.countTeamCompletedToday(deptId); + if (CollectionUtils.isNotEmpty(teamData)) { + int totalCompleted = 0; + int maxCompleted = 0; + for (Map row : teamData) { + Object uidObj = row.get("userId"); + Object cntObj = row.get("completedCount"); + int count = cntObj != null ? ((Number) cntObj).intValue() : 0; + totalCompleted += count; + if (count > maxCompleted) { + maxCompleted = count; + } + if (uidObj != null && userId.equals(((Number) uidObj).longValue())) { + teamProgress.setUserCompletedCount(count); + } + } + int memberCount = teamData.size(); + teamProgress.setTeamMemberCount(memberCount); + teamProgress.setTeamAverage(memberCount > 0 + ? Math.round(totalCompleted * 10.0 / memberCount) / 10.0 : 0.0); + teamProgress.setTeamMax(maxCompleted); + } + } catch (Exception e) { + Log.warn("查询班组完成统计失败", e); + } + return teamProgress; + } + + @Override + public TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + Long deptId = SecurityFrameworkUtils.getDeptId(); + List roleIds = roleApi.getRoleIdsByUserId(userId); + + // 1. 从字典读取配置 + double distWeight = 0.6, effWeight = 0.4, threshold = 2000; + double clampMin = 0.3, clampMax = 3.0; + try { + List configs = dictDataApi.getDictDataList(DictTypeConstants.TASK_SORT_WEIGHT_CONFIG); + if (CollectionUtils.isNotEmpty(configs)) { + for (DictDataRespDTO c : configs) { + if ("距离权重".equals(c.getLabel())) distWeight = Double.parseDouble(c.getValue()); + else if ("效率权重".equals(c.getLabel())) effWeight = Double.parseDouble(c.getValue()); + else if ("效率比上限".equals(c.getLabel())) clampMax = Double.parseDouble(c.getValue()); + else if ("效率比下限".equals(c.getLabel())) clampMin = Double.parseDouble(c.getValue()); + } + } + List speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG); + if (CollectionUtils.isNotEmpty(speedConfigs)) { + for (DictDataRespDTO c : speedConfigs) { + if ("步行可达距离阈值(m)".equals(c.getLabel())) threshold = Double.parseDouble(c.getValue()); + } + } + } catch (Exception e) { + Log.warn("读取智能排序配置失败", e); + } + + // 2. 查询今日待办任务 + List allTasks = homepageSummaryMapper + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, LocalDateTime.now()); + + // 3. 查询效率矩阵 + Map effMap = loadEfficiencyMap(userId); + + // 4. 为每个任务打分 + List tasksWithGps = new ArrayList<>(); + List tasksWithoutGps = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(allTasks)) { + for (TaskDistanceSortRespVO task : allTasks) { + if (task.getTaskLat() != null && task.getTaskLon() != null) { + double dist = GpsDistanceUtil.haversineDistance( + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(), + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue()); + double distScore = Math.max(0, 1 - dist / threshold); + task.setDistanceScore(Math.round(distScore * 100.0) / 100.0); + + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType()); + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax); + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0); + + task.setCompositeScore(Math.round((distWeight * distScore + effWeight * effScore) * 100.0) / 100.0); + tasksWithGps.add(task); + } else { + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType()); + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax); + task.setDistanceScore(0.0); + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0); + task.setCompositeScore(Math.round((effWeight * effScore) * 100.0) / 100.0); + tasksWithoutGps.add(task); + } + } + } + + // 5. 排序:有GPS按综合分降序,无GPS追加末尾按效率分降序 + tasksWithGps.sort((a, b) -> Double.compare( + b.getCompositeScore() != null ? b.getCompositeScore() : 0, + a.getCompositeScore() != null ? a.getCompositeScore() : 0)); + tasksWithoutGps.sort((a, b) -> Double.compare( + b.getEfficiencyScore() != null ? b.getEfficiencyScore() : 0, + a.getEfficiencyScore() != null ? a.getEfficiencyScore() : 0)); + + List sortedTasks = new ArrayList<>(); + int order = 0; + for (TaskDistanceSortRespVO t : tasksWithGps) { + t.setSortOrder(++order); + int distMeters = (int) Math.round(GpsDistanceUtil.haversineDistance( + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(), + t.getTaskLat().doubleValue(), t.getTaskLon().doubleValue())); + t.setDistance(distMeters); + sortedTasks.add(t); + } + for (TaskDistanceSortRespVO t : tasksWithoutGps) { + t.setSortOrder(++order); + t.setDistance(null); + sortedTasks.add(t); + } + + // 6. 班组进度 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId); + + // 7. 组装结果 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO(); + result.setSortedTasks(sortedTasks); + result.setTeamProgress(teamProgress); + result.setTotalPendingTasks(sortedTasks.size()); + int totalDist = 0; + for (TaskDistanceSortRespVO t : sortedTasks) { + if (t.getDistance() != null) totalDist += t.getDistance(); + } + result.setEstimatedTotalDistance(totalDist); + return result; + } + + /** + * 加载当前用户当月效率矩阵 + */ + private Map loadEfficiencyMap(Long userId) { + Map map = new LinkedHashMap<>(); + try { + String statPeriod = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM")); + List list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, statPeriod); + if (CollectionUtils.isEmpty(list)) { + // fallback: 查最近一个周期的数据 + String latestPeriod = userTaskEfficiencyMapper.selectLatestPeriod(userId); + if (latestPeriod != null) { + list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, latestPeriod); + } + } + if (CollectionUtils.isNotEmpty(list)) { + for (UserTaskEfficiencyDO e : list) { + String key = e.getTaskCategory() + "_" + e.getTaskType(); + map.put(key, e.getEfficiencyRatio() != null ? e.getEfficiencyRatio() : BigDecimal.ONE); + } + } + } catch (Exception e) { + Log.warn("加载效率矩阵失败", e); + } + return map; + } + + /** + * 从效率矩阵查找,先精确匹配再 fallback 大类 + */ + private BigDecimal lookupEfficiency(Map map, String category, String subType) { + // 精确匹配 + String exactKey = category + "_" + subType; + if (map.containsKey(exactKey)) return map.get(exactKey); + // fallback: 大类汇总 + String fallbackKey = category + "_ALL"; + if (map.containsKey(fallbackKey)) return map.get(fallbackKey); + // 无数据:中性 + return BigDecimal.ONE; + } + + private double clamp(double val, double min, double max) { + if (val > max) return max; + if (val < min) return min; + return val; + } } diff --git a/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GpsDistanceUtil.java b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GpsDistanceUtil.java new file mode 100644 index 0000000..2dde886 --- /dev/null +++ b/urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GpsDistanceUtil.java @@ -0,0 +1,32 @@ +package com.zteits.urbanops.module.garden.util; + +/** + * GPS 距离计算工具类 + * + * @author + */ +public class GpsDistanceUtil { + + /** 地球半径(米) */ + private static final double EARTH_RADIUS_M = 6_371_000; + + /** + * Haversine 公式计算两点之间的球面距离 + * + * @param lat1 点1纬度 + * @param lon1 点1经度 + * @param lat2 点2纬度 + * @param lon2 点2经度 + * @return 两点之间的距离(米) + */ + public static double haversineDistance(double lat1, double lon1, + double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_M * c; + } +} diff --git a/urbanops-module-garden/src/main/resources/mapper/UserTaskEfficiencyMapper.xml b/urbanops-module-garden/src/main/resources/mapper/UserTaskEfficiencyMapper.xml new file mode 100644 index 0000000..c14493a --- /dev/null +++ b/urbanops-module-garden/src/main/resources/mapper/UserTaskEfficiencyMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + DELETE FROM garden_user_task_efficiency + WHERE stat_period = #{statPeriod} + + + + + + + + + + + + diff --git a/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml b/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml index 0760bc6..2cc3067 100644 --- a/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml +++ b/urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml @@ -615,5 +615,111 @@ GROUP BY w.worker_company_id, d.name + + + + diff --git a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java index 006c12a..a539820 100644 --- a/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java +++ b/urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java @@ -32,8 +32,8 @@ public interface DictTypeConstants { String SYSTEM_IS_INNER = "system_is_inner";//是否是内部员工 + String TRAVEL_SPEED_CONFIG = "travel_speed_config"; // 出行速度配置 - - + String TASK_SORT_WEIGHT_CONFIG = "task_sort_weight_config"; // 智能排序权重配置 } diff --git a/urbanops-server/src/main/resources/application.yaml b/urbanops-server/src/main/resources/application.yaml index e5f485b..66d32b9 100644 --- a/urbanops-server/src/main/resources/application.yaml +++ b/urbanops-server/src/main/resources/application.yaml @@ -271,7 +271,7 @@ urbanops: - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 security: permit-all_urls: - - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 + - /admin-api/mp/open/** - /admin-api/garden/device-user/** api-encrypt: enable: true # 是否开启 API 加密