Commit 5618a89bbf8c57aff6768a9239ca35bd29aab7cf

Authored by 王彪总
1 parent 72b62c4c

feat(garden): 新增任务距离排序和智能排序功能

- 实现任务按距离排序和班组进度统计功能
- 添加任务智能排序(距离+个人效率加权)功能
- 新增GPS距离计算工具类和相关数据传输对象
- 添加出行速度配置和智能排序权重配置字典类型
- 实现用户任务效率预计算定时任务
- 优化API签名切面注解使用方式
- 添加微信公众号开放平台回调接口免登录配置
Showing 18 changed files with 1089 additions and 14 deletions
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 {
37 37  
38 38 private final ApiSignatureRedisDAO signatureRedisDAO;
39 39  
40   - @Before("@annotation(signature)")
  40 + @Before("@within(signature)")
41 41 public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
42 42 // 1. 验证通过,直接结束
43 43 if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
... ... @@ -171,4 +171,4 @@ public class ApiSignatureAspect {
171 171 return sortedMap;
172 172 }
173 173  
174   -}
175 174 \ No newline at end of file
  175 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/AppHomepageSummaryController.java
... ... @@ -2,7 +2,10 @@ package com.zteits.urbanops.module.garden.controller.app.homepage;
2 2  
3 3 import com.zteits.urbanops.framework.common.pojo.CommonResult;
4 4 import com.zteits.urbanops.framework.common.pojo.PageResult;
5   -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*;
  5 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryPageReqVO;
  6 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryReqVO;
  7 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo;
  8 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo;
6 9 import com.zteits.urbanops.module.garden.service.homepage.HomepageSummaryService;
7 10 import io.swagger.v3.oas.annotations.Operation;
8 11 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -12,6 +15,7 @@ import jakarta.validation.Valid;
12 15 import org.springframework.web.bind.annotation.*;
13 16  
14 17 import java.util.List;
  18 +import java.util.Map;
15 19  
16 20 import static com.zteits.urbanops.framework.common.pojo.CommonResult.success;
17 21  
... ... @@ -52,4 +56,16 @@ public class AppHomepageSummaryController {
52 56 public CommonResult<AppWorkOrderSummaryRspVo> iWorkOrderSummary(@Valid @RequestBody AppWorkOrderSummaryReqVo req) {
53 57 return success(homepageSummaryService.iWorkOrderSummary(req));
54 58 }
  59 +
  60 + @PostMapping("/taskDetailsWithDistance")
  61 + @Operation(summary = "任务待办(按距离排序 + 班组进度)")
  62 + public CommonResult<TaskDistanceSortResultVO> queryTaskDetailsWithDistance(@Valid @RequestBody AppTaskDistanceSortReqVO req) {
  63 + return success(homepageSummaryService.queryTaskDetailsWithDistance(req));
  64 + }
  65 +
  66 + @PostMapping("/taskDetailsWithSmartSort")
  67 + @Operation(summary = "任务待办(智能排序:距离 + 个人效率加权)")
  68 + public CommonResult<TaskDistanceSortResultVO> queryTaskDetailsWithSmartSort(@Valid @RequestBody AppTaskSmartSortReqVO req) {
  69 + return success(homepageSummaryService.queryTaskDetailsWithSmartSort(req));
  70 + }
55 71 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskDistanceSortReqVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.app.homepage.vo;
  2 +
  3 +import com.zteits.urbanops.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +
  9 +import java.math.BigDecimal;
  10 +
  11 +/**
  12 + * app - 任务距离排序 请求 VO
  13 + *
  14 + * @author
  15 + */
  16 +@Data
  17 +@EqualsAndHashCode(callSuper = true)
  18 +public class AppTaskDistanceSortReqVO extends PageParam {
  19 +
  20 + @Schema(description = "查询类型:1-待办,2-已办", example = "1")
  21 + @NotNull
  22 + public Integer queryType;
  23 +
  24 + @Schema(description = "当前经度", example = "116.397428")
  25 + @NotNull(message = "当前经度不能为空")
  26 + private BigDecimal currentLon;
  27 +
  28 + @Schema(description = "当前纬度", example = "39.909204")
  29 + @NotNull(message = "当前纬度不能为空")
  30 + private BigDecimal currentLat;
  31 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/AppTaskSmartSortReqVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.app.homepage.vo;
  2 +
  3 +import com.zteits.urbanops.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import lombok.EqualsAndHashCode;
  8 +
  9 +import java.math.BigDecimal;
  10 +
  11 +/**
  12 + * app - 任务智能排序 请求 VO
  13 + *
  14 + * @author
  15 + */
  16 +@Data
  17 +@EqualsAndHashCode(callSuper = true)
  18 +public class AppTaskSmartSortReqVO extends PageParam {
  19 +
  20 + @Schema(description = "查询类型:1-待办,2-已办", example = "1")
  21 + @NotNull
  22 + private Integer queryType;
  23 +
  24 + @Schema(description = "当前经度", example = "116.397428")
  25 + @NotNull(message = "当前经度不能为空")
  26 + private BigDecimal currentLon;
  27 +
  28 + @Schema(description = "当前纬度", example = "39.909204")
  29 + @NotNull(message = "当前纬度不能为空")
  30 + private BigDecimal currentLat;
  31 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortRespVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.app.homepage.vo;
  2 +
  3 +import lombok.Data;
  4 +import lombok.EqualsAndHashCode;
  5 +
  6 +import java.math.BigDecimal;
  7 +
  8 +/**
  9 + * app - 任务距离排序 单个任务响应 VO
  10 + *
  11 + * @author
  12 + */
  13 +@Data
  14 +@EqualsAndHashCode(callSuper = true)
  15 +public class TaskDistanceSortRespVO extends TaskDetailsRspVo {
  16 +
  17 + /** 任务类型:inspection / maintain / workorder */
  18 + private String taskType;
  19 +
  20 + /** 任务点经度 */
  21 + private BigDecimal taskLon;
  22 +
  23 + /** 任务点纬度 */
  24 + private BigDecimal taskLat;
  25 +
  26 + /** 从上一点到本任务的距离(米),无 GPS 则为 null */
  27 + private Integer distance;
  28 +
  29 + /** 从起点累计距离(米) */
  30 + private Integer cumulativeDistance;
  31 +
  32 + /** 预估步行时间(分钟) */
  33 + private Integer estimatedWalkingMinutes;
  34 +
  35 + /** 预估三轮车时间(分钟) */
  36 + private Integer estimatedTricycleMinutes;
  37 +
  38 + /** 排序序号:1, 2, 3... */
  39 + private Integer sortOrder;
  40 +
  41 + /** 任务子类型:plan_type_id 或 order_type,用于效率矩阵匹配 */
  42 + private String taskSubType;
  43 +
  44 + /** 距离得分 [0, 1] */
  45 + private Double distanceScore;
  46 +
  47 + /** 效率得分(已截断) */
  48 + private Double efficiencyScore;
  49 +
  50 + /** 综合得分 */
  51 + private Double compositeScore;
  52 +
  53 + /** 用户历史平均完成时长(分钟) */
  54 + private BigDecimal userAvgMinutes;
  55 +
  56 + /** 班组平均完成时长(分钟) */
  57 + private BigDecimal teamAvgMinutes;
  58 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TaskDistanceSortResultVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.app.homepage.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * app - 任务距离排序 顶层返回 VO
  9 + *
  10 + * @author
  11 + */
  12 +@Data
  13 +public class TaskDistanceSortResultVO {
  14 +
  15 + /** 排序后的任务列表 */
  16 + private List<TaskDistanceSortRespVO> sortedTasks;
  17 +
  18 + /** 班组内进度统计 */
  19 + private TeamProgressVO teamProgress;
  20 +
  21 + /** 预估总距离(米) */
  22 + private Integer estimatedTotalDistance;
  23 +
  24 + /** 预估总步行时间(分钟) */
  25 + private Integer estimatedTotalWalkingMinutes;
  26 +
  27 + /** 预估总三轮车时间(分钟) */
  28 + private Integer estimatedTotalTricycleMinutes;
  29 +
  30 + /** 总待办任务数 */
  31 + private Integer totalPendingTasks;
  32 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/controller/app/homepage/vo/TeamProgressVO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.controller.app.homepage.vo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * app - 班组内进度统计 VO
  7 + *
  8 + * @author
  9 + */
  10 +@Data
  11 +public class TeamProgressVO {
  12 +
  13 + /** 当前用户今日已完成数 */
  14 + private Integer userCompletedCount;
  15 +
  16 + /** 班组均值 */
  17 + private Double teamAverage;
  18 +
  19 + /** 班组内最高 */
  20 + private Integer teamMax;
  21 +
  22 + /** 班组总人数 */
  23 + private Integer teamMemberCount;
  24 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/dataobject/UserTaskEfficiencyDO.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.dal.dataobject;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.KeySequence;
  4 +import com.baomidou.mybatisplus.annotation.TableId;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import com.zteits.urbanops.framework.mybatis.core.dataobject.BaseDO;
  7 +import lombok.*;
  8 +
  9 +import java.math.BigDecimal;
  10 +
  11 +/**
  12 + * 用户任务效率预计算 DO
  13 + *
  14 + * @author
  15 + */
  16 +@TableName("garden_user_task_efficiency")
  17 +@KeySequence("garden_user_task_efficiency_seq")
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +@Builder
  22 +@NoArgsConstructor
  23 +@AllArgsConstructor
  24 +public class UserTaskEfficiencyDO extends BaseDO {
  25 +
  26 + @TableId
  27 + private Long id;
  28 +
  29 + /** 用户ID */
  30 + private Long userId;
  31 +
  32 + /** 部门ID(班组) */
  33 + private Long deptId;
  34 +
  35 + /** 任务大类:inspection / maintain / workorder */
  36 + private String taskCategory;
  37 +
  38 + /** 任务类型(细粒度):plan_type_id 或 order_type,ALL 表示大类汇总 */
  39 + private String taskType;
  40 +
  41 + /** 6个自然月内完成任务数 */
  42 + private Integer completedCount;
  43 +
  44 + /** 总耗时(分钟) */
  45 + private Long totalMinutes;
  46 +
  47 + /** 个人平均完成时长(分钟) */
  48 + private BigDecimal avgMinutes;
  49 +
  50 + /** 班组平均完成时长(分钟) */
  51 + private BigDecimal teamAvgMinutes;
  52 +
  53 + /** 效率比 = teamAvg / userAvg */
  54 + private BigDecimal efficiencyRatio;
  55 +
  56 + /** 统计周期 yyyy-MM */
  57 + private String statPeriod;
  58 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/UserTaskEfficiencyMapper.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.dal.mysql;
  2 +
  3 +import com.zteits.urbanops.framework.mybatis.core.mapper.BaseMapperX;
  4 +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +import org.apache.ibatis.annotations.Param;
  7 +
  8 +import java.util.List;
  9 +import java.util.Map;
  10 +
  11 +/**
  12 + * 用户任务效率 Mapper
  13 + *
  14 + * @author
  15 + */
  16 +@Mapper
  17 +public interface UserTaskEfficiencyMapper extends BaseMapperX<UserTaskEfficiencyDO> {
  18 +
  19 + /**
  20 + * 根据用户和统计周期查询效率数据
  21 + */
  22 + List<UserTaskEfficiencyDO> selectByUserIdAndPeriod(@Param("userId") Long userId,
  23 + @Param("statPeriod") String statPeriod);
  24 +
  25 + /**
  26 + * 查询最近一条有数据的统计周期
  27 + */
  28 + String selectLatestPeriod(@Param("userId") Long userId);
  29 +
  30 + /**
  31 + * 删除指定统计周期的数据(幂等重跑)
  32 + */
  33 + int deleteByPeriod(@Param("statPeriod") String statPeriod);
  34 +
  35 + /** 巡查计划效率统计 */
  36 + List<Map<String, Object>> statInspectionEfficiency(@Param("beginTime") String beginTime);
  37 +
  38 + /** 养护计划效率统计 */
  39 + List<Map<String, Object>> statMaintainEfficiency(@Param("beginTime") String beginTime);
  40 +
  41 + /** 工单效率统计 */
  42 + List<Map<String, Object>> statWorkOrderEfficiency(@Param("beginTime") String beginTime);
  43 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/dal/mysql/homepage/HomepageSummaryMapper.java
... ... @@ -6,10 +6,10 @@ import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitTask
6 6 import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitWorkOrderRespVo;
7 7 import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.CommitWorkOrderSourceRespVo;
8 8 import com.zteits.urbanops.module.garden.controller.admin.homepage.vo.WorkOrderTrendRespVo;
9   -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppWorkOrderSummaryVo;
10 9 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.CommonTaskStatusVo;
11 10 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo;
12 11 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo;
  12 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDistanceSortRespVO;
13 13 import com.zteits.urbanops.module.garden.dal.dataobject.departmentcost.DepartmentCostDO;
14 14 import com.zteits.urbanops.module.garden.dal.dataobject.taskstatistics.TaskStatisticsDO;
15 15 import org.apache.ibatis.annotations.Mapper;
... ... @@ -216,4 +216,17 @@ public interface HomepageSummaryMapper extends BaseMapperX&lt;DepartmentCostDO&gt; {
216 216 * @Return
217 217 */
218 218 List<AppWorkOrderSummaryVo> countWorkOrder(@Param("queryType") String queryType,@Param("userId") Long userId, @Param("beginTime") String beginTime, @Param("endTime") String endTime);
  219 +
  220 + /**
  221 + * 查询今日待办任务(含 GPS 坐标),三表 UNION
  222 + */
  223 + List<TaskDistanceSortRespVO> queryPendingTaskDetailsWithGps(@Param("roleIds") List<Long> roleIds,
  224 + @Param("userId") Long userId,
  225 + @Param("deptId") Long deptId,
  226 + @Param("currentDate") LocalDateTime currentDate);
  227 +
  228 + /**
  229 + * 统计今日同班组(同 dept_id)成员完成任务数
  230 + */
  231 + List<Map<String, Object>> countTeamCompletedToday(@Param("deptId") Long deptId);
219 232 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/job/TaskEfficiencyCalculateJob.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.job;
  2 +
  3 +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO;
  4 +import com.zteits.urbanops.module.garden.dal.mysql.UserTaskEfficiencyMapper;
  5 +import jakarta.annotation.Resource;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import java.math.BigDecimal;
  10 +import java.math.RoundingMode;
  11 +import java.time.LocalDate;
  12 +import java.time.LocalDateTime;
  13 +import java.time.format.DateTimeFormatter;
  14 +import java.util.*;
  15 +
  16 +/**
  17 + * 用户任务效率预计算定时任务
  18 + * 每天凌晨 2:00 执行,统计最近 6 个自然月的用户任务完成效率
  19 + *
  20 + * @author
  21 + */
  22 +@Slf4j
  23 +@Component
  24 +public class TaskEfficiencyCalculateJob {
  25 +
  26 + @Resource
  27 + private UserTaskEfficiencyMapper userTaskEfficiencyMapper;
  28 +
  29 + public void execute() {
  30 + String statPeriod = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
  31 + log.info("TaskEfficiencyCalculateJob start, statPeriod={}", statPeriod);
  32 + try {
  33 + // 1. 删除本月旧数据
  34 + userTaskEfficiencyMapper.deleteByPeriod(statPeriod);
  35 +
  36 + // 2. 6个月窗口
  37 + String beginTime = LocalDate.now().minusMonths(6).withDayOfMonth(1) + " 00:00:00";
  38 +
  39 + // 3. 三类任务分别统计
  40 + List<UserTaskEfficiencyDO> all = new ArrayList<>();
  41 + all.addAll(mapRows(userTaskEfficiencyMapper.statInspectionEfficiency(beginTime), statPeriod));
  42 + all.addAll(mapRows(userTaskEfficiencyMapper.statMaintainEfficiency(beginTime), statPeriod));
  43 + all.addAll(mapRows(userTaskEfficiencyMapper.statWorkOrderEfficiency(beginTime), statPeriod));
  44 +
  45 + if (all.isEmpty()) {
  46 + log.info("TaskEfficiencyCalculateJob done, no data");
  47 + return;
  48 + }
  49 +
  50 + // 4. 计算班组均值
  51 + calcTeamAverage(all);
  52 +
  53 + // 5. 计算效率比
  54 + for (UserTaskEfficiencyDO e : all) {
  55 + e.setEfficiencyRatio(calcRatio(e.getAvgMinutes(), e.getTeamAvgMinutes()));
  56 + }
  57 +
  58 + // 6. 生成大类汇总(ALL)
  59 + all.addAll(buildCategorySummary(all, statPeriod));
  60 +
  61 + // 7. 批量写入
  62 + LocalDateTime now = LocalDateTime.now();
  63 + for (UserTaskEfficiencyDO e : all) {
  64 + e.setCreator("job");
  65 + e.setUpdater("job");
  66 + e.setCreateTime(now);
  67 + e.setUpdateTime(now);
  68 + userTaskEfficiencyMapper.insert(e);
  69 + }
  70 +
  71 + log.info("TaskEfficiencyCalculateJob done, total={}", all.size());
  72 + } catch (Exception e) {
  73 + log.error("TaskEfficiencyCalculateJob error", e);
  74 + }
  75 + }
  76 +
  77 + private List<UserTaskEfficiencyDO> mapRows(List<Map<String, Object>> rows, String statPeriod) {
  78 + List<UserTaskEfficiencyDO> result = new ArrayList<>();
  79 + if (rows == null || rows.isEmpty()) return result;
  80 + for (Map<String, Object> row : rows) {
  81 + UserTaskEfficiencyDO e = new UserTaskEfficiencyDO();
  82 + e.setUserId(toLong(row.get("user_id")));
  83 + e.setDeptId(toLong(row.get("dept_id")));
  84 + e.setTaskCategory((String) row.get("task_category"));
  85 + e.setTaskType(String.valueOf(row.get("task_type")));
  86 + e.setCompletedCount(toInt(row.get("cnt")));
  87 + e.setTotalMinutes(toLong(row.get("total_min")));
  88 + e.setAvgMinutes(toDecimal(row.get("avg_min")));
  89 + e.setStatPeriod(statPeriod);
  90 + result.add(e);
  91 + }
  92 + return result;
  93 + }
  94 +
  95 + private void calcTeamAverage(List<UserTaskEfficiencyDO> all) {
  96 + Map<String, List<UserTaskEfficiencyDO>> groups = new LinkedHashMap<>();
  97 + for (UserTaskEfficiencyDO e : all) {
  98 + String key = e.getDeptId() + "_" + e.getTaskCategory() + "_" + e.getTaskType();
  99 + groups.computeIfAbsent(key, k -> new ArrayList<>()).add(e);
  100 + }
  101 + for (List<UserTaskEfficiencyDO> group : groups.values()) {
  102 + long totalMin = 0;
  103 + int totalCnt = 0;
  104 + for (UserTaskEfficiencyDO e : group) {
  105 + totalMin += e.getTotalMinutes() != null ? e.getTotalMinutes() : 0;
  106 + totalCnt += e.getCompletedCount() != null ? e.getCompletedCount() : 0;
  107 + }
  108 + BigDecimal teamAvg = totalCnt > 0
  109 + ? BigDecimal.valueOf(totalMin).divide(BigDecimal.valueOf(totalCnt), 2, RoundingMode.HALF_UP)
  110 + : BigDecimal.ZERO;
  111 + for (UserTaskEfficiencyDO e : group) {
  112 + e.setTeamAvgMinutes(teamAvg);
  113 + }
  114 + }
  115 + }
  116 +
  117 + private BigDecimal calcRatio(BigDecimal userAvg, BigDecimal teamAvg) {
  118 + if (userAvg == null || teamAvg == null
  119 + || userAvg.compareTo(BigDecimal.ZERO) <= 0
  120 + || teamAvg.compareTo(BigDecimal.ZERO) <= 0) {
  121 + return BigDecimal.ONE;
  122 + }
  123 + BigDecimal ratio = teamAvg.divide(userAvg, 4, RoundingMode.HALF_UP);
  124 + if (ratio.compareTo(new BigDecimal("3.0")) > 0) return new BigDecimal("3.0");
  125 + if (ratio.compareTo(new BigDecimal("0.3")) < 0) return new BigDecimal("0.3");
  126 + return ratio;
  127 + }
  128 +
  129 + private List<UserTaskEfficiencyDO> buildCategorySummary(List<UserTaskEfficiencyDO> all, String statPeriod) {
  130 + Map<String, List<UserTaskEfficiencyDO>> catMap = new LinkedHashMap<>();
  131 + for (UserTaskEfficiencyDO e : all) {
  132 + String key = e.getUserId() + "_" + e.getDeptId() + "_" + e.getTaskCategory();
  133 + catMap.computeIfAbsent(key, k -> new ArrayList<>()).add(e);
  134 + }
  135 + List<UserTaskEfficiencyDO> summaries = new ArrayList<>();
  136 + for (List<UserTaskEfficiencyDO> group : catMap.values()) {
  137 + UserTaskEfficiencyDO s = new UserTaskEfficiencyDO();
  138 + s.setUserId(group.get(0).getUserId());
  139 + s.setDeptId(group.get(0).getDeptId());
  140 + s.setTaskCategory(group.get(0).getTaskCategory());
  141 + s.setTaskType("ALL");
  142 + s.setStatPeriod(statPeriod);
  143 + long totalMin = 0;
  144 + int totalCnt = 0;
  145 + for (UserTaskEfficiencyDO e : group) {
  146 + totalMin += e.getTotalMinutes() != null ? e.getTotalMinutes() : 0;
  147 + totalCnt += e.getCompletedCount() != null ? e.getCompletedCount() : 0;
  148 + }
  149 + s.setCompletedCount(totalCnt);
  150 + s.setTotalMinutes(totalMin);
  151 + s.setAvgMinutes(totalCnt > 0
  152 + ? BigDecimal.valueOf(totalMin).divide(BigDecimal.valueOf(totalCnt), 2, RoundingMode.HALF_UP)
  153 + : BigDecimal.ZERO);
  154 + s.setTeamAvgMinutes(group.get(0).getTeamAvgMinutes());
  155 + s.setEfficiencyRatio(calcRatio(s.getAvgMinutes(), s.getTeamAvgMinutes()));
  156 + summaries.add(s);
  157 + }
  158 + return summaries;
  159 + }
  160 +
  161 + private Long toLong(Object o) {
  162 + if (o == null) return 0L;
  163 + return o instanceof Number ? ((Number) o).longValue() : Long.parseLong(o.toString());
  164 + }
  165 + private Integer toInt(Object o) {
  166 + if (o == null) return 0;
  167 + return o instanceof Number ? ((Number) o).intValue() : Integer.parseInt(o.toString());
  168 + }
  169 + private BigDecimal toDecimal(Object o) {
  170 + if (o == null) return BigDecimal.ZERO;
  171 + return o instanceof BigDecimal ? (BigDecimal) o : new BigDecimal(o.toString());
  172 + }
  173 +}
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryService.java
1 1 package com.zteits.urbanops.module.garden.service.homepage;
2 2  
3 3 import com.zteits.urbanops.framework.common.pojo.PageResult;
4   -import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*;
  4 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryPageReqVO;
  5 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppHomePageSummaryReqVO;
  6 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppTaskDistanceSortReqVO;
  7 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppTaskSmartSortReqVO;
  8 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo;
  9 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo;
  10 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDistanceSortResultVO;
5 11  
6 12 import java.util.List;
  13 +import java.util.Map;
7 14  
8 15 /**
9 16 * @Classname HomepageSummaryService
... ... @@ -48,4 +55,16 @@ public interface HomepageSummaryService {
48 55 AppWorkOrderSummaryRspVo iWorkOrderSummary(AppWorkOrderSummaryReqVo req);
49 56  
50 57  
  58 + /**
  59 + * 任务待办(按距离排序 + 班组进度)
  60 + *
  61 + * @param req 请求参数(当前 GPS 坐标)
  62 + * @return 排序后的任务列表 + 班组进度统计
  63 + */
  64 + TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req);
  65 +
  66 + /**
  67 + * 任务待办(智能排序:距离 + 个人效率加权)
  68 + */
  69 + TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req);
51 70 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/service/homepage/HomepageSummaryServiceImpl.java
... ... @@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
5 5 import com.baomidou.mybatisplus.core.metadata.IPage;
6 6 import com.esotericsoftware.minlog.Log;
7 7 import com.zteits.urbanops.framework.common.pojo.PageResult;
  8 +import com.zteits.urbanops.framework.common.util.date.DateUtils;
8 9 import com.zteits.urbanops.framework.common.util.date.LocalDateTimeUtils;
  10 +import com.zteits.urbanops.framework.mybatis.core.query.QueryWrapperX;
9 11 import com.zteits.urbanops.framework.mybatis.core.util.MyBatisUtils;
10 12 import com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils;
11 13 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*;
... ... @@ -15,7 +17,7 @@ import com.zteits.urbanops.module.garden.dal.mysql.taskstatistics.TaskStatistics
15 17 import com.zteits.urbanops.module.system.api.dept.DeptApi;
16 18 import com.zteits.urbanops.module.system.api.permission.RoleApi;
17 19 import com.zteits.urbanops.module.system.api.user.AdminUserApi;
18   -import com.zteits.urbanops.module.system.enums.common.CommonConstants;
  20 +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO;
19 21 import jakarta.annotation.Resource;
20 22 import org.apache.commons.collections4.CollectionUtils;
21 23 import org.apache.commons.lang3.StringUtils;
... ... @@ -26,12 +28,11 @@ import java.text.DateFormat;
26 28 import java.text.ParseException;
27 29 import java.text.SimpleDateFormat;
28 30 import java.time.LocalDateTime;
  31 +import java.time.LocalTime;
29 32 import java.time.format.DateTimeFormatter;
30 33 import java.util.*;
31 34 import java.util.stream.Collectors;
32 35  
33   -import static com.zteits.urbanops.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
34   -
35 36 /**
36 37 * @Classname HomepageSummaryServiceImpl
37 38 * @Description 首页统计实现
... ... @@ -57,9 +58,15 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
57 58 @Resource
58 59 private DeptApi deptApi;
59 60  
  61 + @Resource
  62 + private DictDataApi dictDataApi;
  63 +
  64 + @Resource
  65 + private UserTaskEfficiencyMapper userTaskEfficiencyMapper;
  66 +
60 67 @Override
61 68 public List<TaskCompletionStatusRspVo> taskCompletionSummary(AppHomePageSummaryReqVO req) {
62   - Long userId = getLoginUserId();
  69 + Long userId = SecurityFrameworkUtils.getLoginUserId();
63 70 //巡查任务和养护任务已完成,未完成数量统计
64 71 //返回数据初始化
65 72 List<String> days = getDays(req.getBeginTime(), req.getEndTime());
... ... @@ -105,7 +112,7 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
105 112  
106 113 @Override
107 114 public PageResult<TaskDetailsRspVo> queryTaskDetails(AppHomePageSummaryPageReqVO req) {
108   - Long userId = getLoginUserId();
  115 + Long userId = SecurityFrameworkUtils.getLoginUserId();
109 116 IPage<TaskDetailsRspVo> mpPage = MyBatisUtils.buildPage(req);
110 117 if (req.getQueryType() == 2) {
111 118 //已完成
... ... @@ -113,7 +120,7 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
113 120 } else {
114 121 //待办
115 122 Long deptId = SecurityFrameworkUtils.getDeptId();
116   - List<Long> roleIds = roleApi.getRoleIdsByUserId(getLoginUserId());
  123 + List<Long> roleIds = roleApi.getRoleIdsByUserId(SecurityFrameworkUtils.getLoginUserId());
117 124 mpPage = homepageSummaryMapper.queryPendingTaskDetails(mpPage, roleIds, userId, deptId, LocalDateTime.now());
118 125 }
119 126 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
... ... @@ -401,4 +408,360 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
401 408  
402 409 return days;
403 410 }
  411 +
  412 + @Override
  413 + public TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req) {
  414 + // 1. 获取当前登录用户信息
  415 + Long userId = SecurityFrameworkUtils.getLoginUserId();
  416 + Long deptId = SecurityFrameworkUtils.getDeptId();
  417 + List<Long> roleIds = roleApi.getRoleIdsByUserId(userId);
  418 +
  419 + // 2. 根据 queryType 分流
  420 + List<TaskDistanceSortRespVO> sortedTasks;
  421 + if (req.getQueryType() != null && req.getQueryType() == 2) {
  422 + // ========== 已办:查询已完成任务(无 GPS 距离排序) ==========
  423 + sortedTasks = queryCompletedTasksWithoutGps(userId);
  424 + } else {
  425 + // ========== 待办:查询待办任务 + GPS 贪心最近邻排序 ==========
  426 + sortedTasks = queryPendingTasksWithGpsSort(req, userId, deptId, roleIds);
  427 + }
  428 +
  429 + // 3. 查询班组进度
  430 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId);
  431 +
  432 + // 4. 组装返回结果
  433 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO();
  434 + result.setSortedTasks(sortedTasks);
  435 + result.setTeamProgress(teamProgress);
  436 + result.setTotalPendingTasks(sortedTasks.size());
  437 + int totalDist = 0;
  438 + int totalWalkMin = 0;
  439 + int totalTriMin = 0;
  440 + for (TaskDistanceSortRespVO task : sortedTasks) {
  441 + if (task.getDistance() != null) {
  442 + totalDist += task.getDistance();
  443 + totalWalkMin += (task.getEstimatedWalkingMinutes() != null ? task.getEstimatedWalkingMinutes() : 0);
  444 + totalTriMin += (task.getEstimatedTricycleMinutes() != null ? task.getEstimatedTricycleMinutes() : 0);
  445 + }
  446 + }
  447 + result.setEstimatedTotalDistance(totalDist);
  448 + result.setEstimatedTotalWalkingMinutes(totalWalkMin);
  449 + result.setEstimatedTotalTricycleMinutes(totalTriMin);
  450 +
  451 + return result;
  452 + }
  453 +
  454 + /**
  455 + * 已办任务查询(无 GPS 排序)
  456 + */
  457 + private List<TaskDistanceSortRespVO> queryCompletedTasksWithoutGps(Long userId) {
  458 + IPage<TaskDetailsRspVo> page = MyBatisUtils.buildPage(new AppHomePageSummaryPageReqVO());
  459 + // 设置大分页以获取全部已办任务
  460 + page.setSize(Integer.MAX_VALUE);
  461 + IPage<TaskDetailsRspVo> mpPage = homepageSummaryMapper.queryCompletedTaskDetails(page, userId);
  462 + List<TaskDistanceSortRespVO> result = new ArrayList<>();
  463 + if (mpPage != null && CollectionUtils.isNotEmpty(mpPage.getRecords())) {
  464 + int i = 0;
  465 + for (TaskDetailsRspVo vo : mpPage.getRecords()) {
  466 + TaskDistanceSortRespVO sortVo = new TaskDistanceSortRespVO();
  467 + sortVo.setTaskName(vo.getTaskName());
  468 + sortVo.setBusiDateTime(vo.getBusiDateTime());
  469 + sortVo.setPressingType(vo.getPressingType());
  470 + sortVo.setSortOrder(++i);
  471 + sortVo.setDistance(null);
  472 + sortVo.setCumulativeDistance(null);
  473 + sortVo.setEstimatedWalkingMinutes(null);
  474 + sortVo.setEstimatedTricycleMinutes(null);
  475 + result.add(sortVo);
  476 + }
  477 + }
  478 + return result;
  479 + }
  480 +
  481 + /**
  482 + * 待办任务查询 + GPS 贪心最近邻排序
  483 + */
  484 + private List<TaskDistanceSortRespVO> queryPendingTasksWithGpsSort(AppTaskDistanceSortReqVO req,
  485 + Long userId, Long deptId,
  486 + List<Long> roleIds) {
  487 + // 从字典表读取速度配置
  488 + double walkingSpeedKmh = 4.0;
  489 + double tricycleSpeedKmh = 15.0;
  490 + try {
  491 + List<DictDataRespDTO> speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG);
  492 + if (CollectionUtils.isNotEmpty(speedConfigs)) {
  493 + for (DictDataRespDTO config : speedConfigs) {
  494 + if ("步行速度(km/h)".equals(config.getLabel())) {
  495 + walkingSpeedKmh = Double.parseDouble(config.getValue());
  496 + } else if ("三轮车速度(km/h)".equals(config.getLabel())) {
  497 + tricycleSpeedKmh = Double.parseDouble(config.getValue());
  498 + }
  499 + }
  500 + }
  501 + } catch (Exception e) {
  502 + Log.warn("读取出行速度字典配置失败,使用默认值", e);
  503 + }
  504 + double walkingSpeedMperMin = walkingSpeedKmh * 1000 / 60;
  505 + double tricycleSpeedMperMin = tricycleSpeedKmh * 1000 / 60;
  506 +
  507 + // 查询今日待办任务(含 GPS 坐标)
  508 + LocalDateTime now = LocalDateTime.now();
  509 + List<TaskDistanceSortRespVO> allTasks = homepageSummaryMapper
  510 + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, now);
  511 +
  512 + // 分离有 GPS 和无 GPS 任务
  513 + List<TaskDistanceSortRespVO> tasksWithGps = new ArrayList<>();
  514 + List<TaskDistanceSortRespVO> tasksWithoutGps = new ArrayList<>();
  515 + if (CollectionUtils.isNotEmpty(allTasks)) {
  516 + for (TaskDistanceSortRespVO task : allTasks) {
  517 + if (task.getTaskLat() != null && task.getTaskLon() != null) {
  518 + tasksWithGps.add(task);
  519 + } else {
  520 + tasksWithoutGps.add(task);
  521 + }
  522 + }
  523 + }
  524 +
  525 + // 贪心最近邻排序
  526 + List<TaskDistanceSortRespVO> sortedTasks = new ArrayList<>();
  527 + double currentLat = req.getCurrentLat().doubleValue();
  528 + double currentLon = req.getCurrentLon().doubleValue();
  529 + int cumulativeDistance = 0;
  530 + int sortOrder = 0;
  531 +
  532 + List<TaskDistanceSortRespVO> remaining = new ArrayList<>(tasksWithGps);
  533 + while (!remaining.isEmpty()) {
  534 + TaskDistanceSortRespVO nearest = null;
  535 + double minDistance = Double.MAX_VALUE;
  536 + int nearestIndex = -1;
  537 + for (int i = 0; i < remaining.size(); i++) {
  538 + TaskDistanceSortRespVO task = remaining.get(i);
  539 + double dist = GpsDistanceUtil.haversineDistance(
  540 + currentLat, currentLon,
  541 + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue());
  542 + if (dist < minDistance) {
  543 + minDistance = dist;
  544 + nearest = task;
  545 + nearestIndex = i;
  546 + }
  547 + }
  548 + sortOrder++;
  549 + int distMeters = (int) Math.round(minDistance);
  550 + cumulativeDistance += distMeters;
  551 + nearest.setSortOrder(sortOrder);
  552 + nearest.setDistance(distMeters);
  553 + nearest.setCumulativeDistance(cumulativeDistance);
  554 + nearest.setEstimatedWalkingMinutes((int) Math.ceil(distMeters / walkingSpeedMperMin));
  555 + nearest.setEstimatedTricycleMinutes((int) Math.ceil(distMeters / tricycleSpeedMperMin));
  556 + sortedTasks.add(nearest);
  557 +
  558 + currentLat = nearest.getTaskLat().doubleValue();
  559 + currentLon = nearest.getTaskLon().doubleValue();
  560 + remaining.remove(nearestIndex);
  561 + }
  562 +
  563 + // 无 GPS 任务追加到末尾
  564 + if (CollectionUtils.isNotEmpty(tasksWithoutGps)) {
  565 + for (TaskDistanceSortRespVO task : tasksWithoutGps) {
  566 + sortOrder++;
  567 + task.setSortOrder(sortOrder);
  568 + task.setDistance(null);
  569 + task.setCumulativeDistance(null);
  570 + task.setEstimatedWalkingMinutes(null);
  571 + task.setEstimatedTricycleMinutes(null);
  572 + sortedTasks.add(task);
  573 + }
  574 + }
  575 + return sortedTasks;
  576 + }
  577 +
  578 + /**
  579 + * 构建班组进度统计
  580 + */
  581 + private TeamProgressVO buildTeamProgress(Long userId, Long deptId) {
  582 + TeamProgressVO teamProgress = new TeamProgressVO();
  583 + teamProgress.setUserCompletedCount(0);
  584 + teamProgress.setTeamAverage(0.0);
  585 + teamProgress.setTeamMax(0);
  586 + teamProgress.setTeamMemberCount(0);
  587 + try {
  588 + List<Map<String, Object>> teamData = homepageSummaryMapper.countTeamCompletedToday(deptId);
  589 + if (CollectionUtils.isNotEmpty(teamData)) {
  590 + int totalCompleted = 0;
  591 + int maxCompleted = 0;
  592 + for (Map<String, Object> row : teamData) {
  593 + Object uidObj = row.get("userId");
  594 + Object cntObj = row.get("completedCount");
  595 + int count = cntObj != null ? ((Number) cntObj).intValue() : 0;
  596 + totalCompleted += count;
  597 + if (count > maxCompleted) {
  598 + maxCompleted = count;
  599 + }
  600 + if (uidObj != null && userId.equals(((Number) uidObj).longValue())) {
  601 + teamProgress.setUserCompletedCount(count);
  602 + }
  603 + }
  604 + int memberCount = teamData.size();
  605 + teamProgress.setTeamMemberCount(memberCount);
  606 + teamProgress.setTeamAverage(memberCount > 0
  607 + ? Math.round(totalCompleted * 10.0 / memberCount) / 10.0 : 0.0);
  608 + teamProgress.setTeamMax(maxCompleted);
  609 + }
  610 + } catch (Exception e) {
  611 + Log.warn("查询班组完成统计失败", e);
  612 + }
  613 + return teamProgress;
  614 + }
  615 +
  616 + @Override
  617 + public TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req) {
  618 + Long userId = SecurityFrameworkUtils.getLoginUserId();
  619 + Long deptId = SecurityFrameworkUtils.getDeptId();
  620 + List<Long> roleIds = roleApi.getRoleIdsByUserId(userId);
  621 +
  622 + // 1. 从字典读取配置
  623 + double distWeight = 0.6, effWeight = 0.4, threshold = 2000;
  624 + double clampMin = 0.3, clampMax = 3.0;
  625 + try {
  626 + List<DictDataRespDTO> configs = dictDataApi.getDictDataList(DictTypeConstants.TASK_SORT_WEIGHT_CONFIG);
  627 + if (CollectionUtils.isNotEmpty(configs)) {
  628 + for (DictDataRespDTO c : configs) {
  629 + if ("距离权重".equals(c.getLabel())) distWeight = Double.parseDouble(c.getValue());
  630 + else if ("效率权重".equals(c.getLabel())) effWeight = Double.parseDouble(c.getValue());
  631 + else if ("效率比上限".equals(c.getLabel())) clampMax = Double.parseDouble(c.getValue());
  632 + else if ("效率比下限".equals(c.getLabel())) clampMin = Double.parseDouble(c.getValue());
  633 + }
  634 + }
  635 + List<DictDataRespDTO> speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG);
  636 + if (CollectionUtils.isNotEmpty(speedConfigs)) {
  637 + for (DictDataRespDTO c : speedConfigs) {
  638 + if ("步行可达距离阈值(m)".equals(c.getLabel())) threshold = Double.parseDouble(c.getValue());
  639 + }
  640 + }
  641 + } catch (Exception e) {
  642 + Log.warn("读取智能排序配置失败", e);
  643 + }
  644 +
  645 + // 2. 查询今日待办任务
  646 + List<TaskDistanceSortRespVO> allTasks = homepageSummaryMapper
  647 + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, LocalDateTime.now());
  648 +
  649 + // 3. 查询效率矩阵
  650 + Map<String, BigDecimal> effMap = loadEfficiencyMap(userId);
  651 +
  652 + // 4. 为每个任务打分
  653 + List<TaskDistanceSortRespVO> tasksWithGps = new ArrayList<>();
  654 + List<TaskDistanceSortRespVO> tasksWithoutGps = new ArrayList<>();
  655 + if (CollectionUtils.isNotEmpty(allTasks)) {
  656 + for (TaskDistanceSortRespVO task : allTasks) {
  657 + if (task.getTaskLat() != null && task.getTaskLon() != null) {
  658 + double dist = GpsDistanceUtil.haversineDistance(
  659 + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(),
  660 + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue());
  661 + double distScore = Math.max(0, 1 - dist / threshold);
  662 + task.setDistanceScore(Math.round(distScore * 100.0) / 100.0);
  663 +
  664 + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType());
  665 + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax);
  666 + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0);
  667 +
  668 + task.setCompositeScore(Math.round((distWeight * distScore + effWeight * effScore) * 100.0) / 100.0);
  669 + tasksWithGps.add(task);
  670 + } else {
  671 + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType());
  672 + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax);
  673 + task.setDistanceScore(0.0);
  674 + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0);
  675 + task.setCompositeScore(Math.round((effWeight * effScore) * 100.0) / 100.0);
  676 + tasksWithoutGps.add(task);
  677 + }
  678 + }
  679 + }
  680 +
  681 + // 5. 排序:有GPS按综合分降序,无GPS追加末尾按效率分降序
  682 + tasksWithGps.sort((a, b) -> Double.compare(
  683 + b.getCompositeScore() != null ? b.getCompositeScore() : 0,
  684 + a.getCompositeScore() != null ? a.getCompositeScore() : 0));
  685 + tasksWithoutGps.sort((a, b) -> Double.compare(
  686 + b.getEfficiencyScore() != null ? b.getEfficiencyScore() : 0,
  687 + a.getEfficiencyScore() != null ? a.getEfficiencyScore() : 0));
  688 +
  689 + List<TaskDistanceSortRespVO> sortedTasks = new ArrayList<>();
  690 + int order = 0;
  691 + for (TaskDistanceSortRespVO t : tasksWithGps) {
  692 + t.setSortOrder(++order);
  693 + int distMeters = (int) Math.round(GpsDistanceUtil.haversineDistance(
  694 + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(),
  695 + t.getTaskLat().doubleValue(), t.getTaskLon().doubleValue()));
  696 + t.setDistance(distMeters);
  697 + sortedTasks.add(t);
  698 + }
  699 + for (TaskDistanceSortRespVO t : tasksWithoutGps) {
  700 + t.setSortOrder(++order);
  701 + t.setDistance(null);
  702 + sortedTasks.add(t);
  703 + }
  704 +
  705 + // 6. 班组进度
  706 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId);
  707 +
  708 + // 7. 组装结果
  709 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO();
  710 + result.setSortedTasks(sortedTasks);
  711 + result.setTeamProgress(teamProgress);
  712 + result.setTotalPendingTasks(sortedTasks.size());
  713 + int totalDist = 0;
  714 + for (TaskDistanceSortRespVO t : sortedTasks) {
  715 + if (t.getDistance() != null) totalDist += t.getDistance();
  716 + }
  717 + result.setEstimatedTotalDistance(totalDist);
  718 + return result;
  719 + }
  720 +
  721 + /**
  722 + * 加载当前用户当月效率矩阵
  723 + */
  724 + private Map<String, BigDecimal> loadEfficiencyMap(Long userId) {
  725 + Map<String, BigDecimal> map = new LinkedHashMap<>();
  726 + try {
  727 + String statPeriod = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
  728 + List<UserTaskEfficiencyDO> list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, statPeriod);
  729 + if (CollectionUtils.isEmpty(list)) {
  730 + // fallback: 查最近一个周期的数据
  731 + String latestPeriod = userTaskEfficiencyMapper.selectLatestPeriod(userId);
  732 + if (latestPeriod != null) {
  733 + list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, latestPeriod);
  734 + }
  735 + }
  736 + if (CollectionUtils.isNotEmpty(list)) {
  737 + for (UserTaskEfficiencyDO e : list) {
  738 + String key = e.getTaskCategory() + "_" + e.getTaskType();
  739 + map.put(key, e.getEfficiencyRatio() != null ? e.getEfficiencyRatio() : BigDecimal.ONE);
  740 + }
  741 + }
  742 + } catch (Exception e) {
  743 + Log.warn("加载效率矩阵失败", e);
  744 + }
  745 + return map;
  746 + }
  747 +
  748 + /**
  749 + * 从效率矩阵查找,先精确匹配再 fallback 大类
  750 + */
  751 + private BigDecimal lookupEfficiency(Map<String, BigDecimal> map, String category, String subType) {
  752 + // 精确匹配
  753 + String exactKey = category + "_" + subType;
  754 + if (map.containsKey(exactKey)) return map.get(exactKey);
  755 + // fallback: 大类汇总
  756 + String fallbackKey = category + "_ALL";
  757 + if (map.containsKey(fallbackKey)) return map.get(fallbackKey);
  758 + // 无数据:中性
  759 + return BigDecimal.ONE;
  760 + }
  761 +
  762 + private double clamp(double val, double min, double max) {
  763 + if (val > max) return max;
  764 + if (val < min) return min;
  765 + return val;
  766 + }
404 767 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/util/GpsDistanceUtil.java 0 → 100644
  1 +package com.zteits.urbanops.module.garden.util;
  2 +
  3 +/**
  4 + * GPS 距离计算工具类
  5 + *
  6 + * @author
  7 + */
  8 +public class GpsDistanceUtil {
  9 +
  10 + /** 地球半径(米) */
  11 + private static final double EARTH_RADIUS_M = 6_371_000;
  12 +
  13 + /**
  14 + * Haversine 公式计算两点之间的球面距离
  15 + *
  16 + * @param lat1 点1纬度
  17 + * @param lon1 点1经度
  18 + * @param lat2 点2纬度
  19 + * @param lon2 点2经度
  20 + * @return 两点之间的距离(米)
  21 + */
  22 + public static double haversineDistance(double lat1, double lon1,
  23 + double lat2, double lon2) {
  24 + double dLat = Math.toRadians(lat2 - lat1);
  25 + double dLon = Math.toRadians(lon2 - lon1);
  26 + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
  27 + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
  28 + * Math.sin(dLon / 2) * Math.sin(dLon / 2);
  29 + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  30 + return EARTH_RADIUS_M * c;
  31 + }
  32 +}
... ...
urbanops-module-garden/src/main/resources/mapper/UserTaskEfficiencyMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="com.zteits.urbanops.module.garden.dal.mysql.UserTaskEfficiencyMapper">
  4 +
  5 + <select id="selectByUserIdAndPeriod" resultType="com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO">
  6 + SELECT * FROM garden_user_task_efficiency
  7 + WHERE user_id = #{userId}
  8 + AND stat_period = #{statPeriod}
  9 + AND deleted = 0
  10 + </select>
  11 +
  12 + <select id="selectLatestPeriod" resultType="java.lang.String">
  13 + SELECT stat_period FROM garden_user_task_efficiency
  14 + WHERE user_id = #{userId}
  15 + AND deleted = 0
  16 + ORDER BY stat_period DESC
  17 + LIMIT 1
  18 + </select>
  19 +
  20 + <delete id="deleteByPeriod">
  21 + DELETE FROM garden_user_task_efficiency
  22 + WHERE stat_period = #{statPeriod}
  23 + </delete>
  24 +
  25 + <!-- 巡查计划效率统计 -->
  26 + <select id="statInspectionEfficiency" resultType="java.util.Map">
  27 + SELECT c.user_id, u.dept_id, 'inspection' AS task_category,
  28 + p.plan_type_id AS task_type, COUNT(*) AS cnt,
  29 + SUM(TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time)) AS total_min,
  30 + AVG(TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time)) AS avg_min
  31 + FROM garden_inspection_plan_commit c
  32 + INNER JOIN garden_inspection_plan_detail d ON c.batch_no = d.batch_no AND c.user_id = d.user_id
  33 + INNER JOIN garden_inspection_plan p ON c.batch_no = p.batch_no
  34 + INNER JOIN system_users u ON c.user_id = u.id
  35 + WHERE c.finish_time >= #{beginTime}
  36 + AND c.finish_time IS NOT NULL
  37 + AND d.begin_time IS NOT NULL
  38 + AND TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time) BETWEEN 1 AND 1440
  39 + GROUP BY c.user_id, u.dept_id, p.plan_type_id
  40 + </select>
  41 +
  42 + <!-- 养护计划效率统计 -->
  43 + <select id="statMaintainEfficiency" resultType="java.util.Map">
  44 + SELECT c.user_id, u.dept_id, 'maintain' AS task_category,
  45 + p.plan_type_id AS task_type, COUNT(*) AS cnt,
  46 + SUM(TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time)) AS total_min,
  47 + AVG(TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time)) AS avg_min
  48 + FROM garden_maintain_plan_commit c
  49 + INNER JOIN garden_maintain_plan_detail d ON c.batch_no = d.batch_no AND c.user_id = d.user_id
  50 + INNER JOIN garden_maintain_plan p ON c.batch_no = p.batch_no
  51 + INNER JOIN system_users u ON c.user_id = u.id
  52 + WHERE c.finish_time >= #{beginTime}
  53 + AND c.finish_time IS NOT NULL
  54 + AND d.begin_time IS NOT NULL
  55 + AND TIMESTAMPDIFF(MINUTE, d.begin_time, c.finish_time) BETWEEN 1 AND 1440
  56 + GROUP BY c.user_id, u.dept_id, p.plan_type_id
  57 + </select>
  58 +
  59 + <!-- 工单效率统计 -->
  60 + <select id="statWorkOrderEfficiency" resultType="java.util.Map">
  61 + SELECT t.ASSIGNEE_ AS user_id, u.dept_id, 'workorder' AS task_category,
  62 + m.order_type AS task_type, COUNT(*) AS cnt,
  63 + SUM(TIMESTAMPDIFF(MINUTE, t.START_TIME_, t.END_TIME_)) AS total_min,
  64 + AVG(TIMESTAMPDIFF(MINUTE, t.START_TIME_, t.END_TIME_)) AS avg_min
  65 + FROM act_hi_taskinst t
  66 + LEFT JOIN workorder_main_info m ON t.PROC_INST_ID_ = m.process_instance_id AND m.deleted = 0
  67 + INNER JOIN system_users u ON t.ASSIGNEE_ = u.id
  68 + WHERE t.END_TIME_ >= #{beginTime}
  69 + AND t.state_ = 'completed'
  70 + AND t.START_TIME_ IS NOT NULL AND t.END_TIME_ IS NOT NULL
  71 + AND m.order_name IS NOT NULL
  72 + AND TIMESTAMPDIFF(MINUTE, t.START_TIME_, t.END_TIME_) BETWEEN 1 AND 1440
  73 + GROUP BY t.ASSIGNEE_, u.dept_id, m.order_type
  74 + </select>
  75 +
  76 +</mapper>
... ...
urbanops-module-garden/src/main/resources/mapper/homepage/HomepageSummaryMapper.xml
... ... @@ -615,5 +615,111 @@
615 615 GROUP BY w.worker_company_id, d.name
616 616 </select>
617 617  
  618 + <!-- 查询今日待办任务(含 GPS 坐标) -->
  619 + <select id="queryPendingTaskDetailsWithGps" resultType="com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDistanceSortRespVO">
  620 + SELECT taskName, busiDateTime, pressingType, taskLat, taskLon, taskType, taskSubType
  621 + FROM (
  622 + SELECT
  623 + a.plan_name AS taskName,
  624 + c.begin_time AS busiDateTime,
  625 + NULL AS pressingType,
  626 + (CAST(r.starting_latitude AS DECIMAL(18,10))
  627 + + CAST(r.end_latitude AS DECIMAL(18,10))) / 2 AS taskLat,
  628 + (CAST(r.starting_longitude AS DECIMAL(18,10))
  629 + + CAST(r.end_longitude AS DECIMAL(18,10))) / 2 AS taskLon,
  630 + 'inspection' AS taskType,
  631 + a.plan_type_id AS taskSubType
  632 + FROM garden_inspection_plan a
  633 + INNER JOIN garden_inspection_plan_role b ON a.batch_no = b.batch_no
  634 + INNER JOIN garden_inspection_plan_detail c ON a.batch_no = c.batch_no
  635 + LEFT JOIN garden_road r ON a.road_id = r.id AND r.deleted = 0
  636 + WHERE a.deleted = 0
  637 + AND c.finish_state = 1
  638 + AND c.begin_time &lt;= #{currentDate}
  639 + AND c.end_time >= #{currentDate}
  640 + AND a.dept_id = #{deptId}
  641 + AND b.role_id IN
  642 + <foreach collection='roleIds' item='roleId' open='(' close=')' separator=','>
  643 + #{roleId}
  644 + </foreach>
  645 + AND r.starting_latitude IS NOT NULL AND r.starting_latitude != ''
  646 +
  647 + UNION ALL
  648 +
  649 + SELECT
  650 + a.plan_name AS taskName,
  651 + c.begin_time AS busiDateTime,
  652 + NULL AS pressingType,
  653 + (CAST(r.starting_latitude AS DECIMAL(18,10))
  654 + + CAST(r.end_latitude AS DECIMAL(18,10))) / 2 AS taskLat,
  655 + (CAST(r.starting_longitude AS DECIMAL(18,10))
  656 + + CAST(r.end_longitude AS DECIMAL(18,10))) / 2 AS taskLon,
  657 + 'maintain' AS taskType,
  658 + a.plan_type_id AS taskSubType
  659 + FROM garden_maintain_plan a
  660 + INNER JOIN garden_maintain_plan_role b ON a.batch_no = b.batch_no
  661 + INNER JOIN garden_maintain_plan_detail c ON a.batch_no = c.batch_no
  662 + LEFT JOIN garden_road r ON a.road_id = r.id AND r.deleted = 0
  663 + WHERE a.deleted = 0
  664 + AND c.finish_state = 1
  665 + AND c.begin_time &lt;= #{currentDate}
  666 + AND c.end_time >= #{currentDate}
  667 + AND a.dept_id = #{deptId}
  668 + AND b.role_id IN
  669 + <foreach collection='roleIds' item='roleId' open='(' close=')' separator=','>
  670 + #{roleId}
  671 + </foreach>
  672 + AND r.starting_latitude IS NOT NULL AND r.starting_latitude != ''
  673 +
  674 + UNION ALL
  675 +
  676 + SELECT
  677 + m.order_name AS taskName,
  678 + m.create_time AS busiDateTime,
  679 + m.pressing_type AS pressingType,
  680 + m.lat AS taskLat,
  681 + m.lon AS taskLon,
  682 + 'workorder' AS taskType,
  683 + m.order_type AS taskSubType
  684 + FROM act_ru_task t
  685 + LEFT JOIN workorder_main_info m
  686 + ON t.PROC_INST_ID_ = m.process_instance_id AND m.deleted = 0
  687 + WHERE t.SUSPENSION_STATE_ = 1
  688 + AND m.order_name IS NOT NULL
  689 + AND t.ASSIGNEE_ = #{userId}
  690 + ) t
  691 + </select>
  692 +
  693 + <select id="countTeamCompletedToday" resultType="java.util.Map">
  694 + SELECT
  695 + u.id AS userId,
  696 + u.nickname AS userName,
  697 + COALESCE(insp.cnt, 0) + COALESCE(mt.cnt, 0)
  698 + + COALESCE(wo.cnt, 0) AS completedCount
  699 + FROM system_users u
  700 + LEFT JOIN (
  701 + SELECT user_id, COUNT(*) AS cnt
  702 + FROM garden_inspection_plan_commit
  703 + WHERE DATE(finish_time) = CURDATE()
  704 + GROUP BY user_id
  705 + ) insp ON u.id = insp.user_id
  706 + LEFT JOIN (
  707 + SELECT user_id, COUNT(*) AS cnt
  708 + FROM garden_maintain_plan_commit
  709 + WHERE DATE(finish_time) = CURDATE()
  710 + GROUP BY user_id
  711 + ) mt ON u.id = mt.user_id
  712 + LEFT JOIN (
  713 + SELECT t.ASSIGNEE_ AS user_id, COUNT(DISTINCT m.order_no) AS cnt
  714 + FROM act_hi_taskinst t
  715 + LEFT JOIN workorder_main_info m
  716 + ON t.PROC_INST_ID_ = m.process_instance_id AND m.deleted = 0
  717 + WHERE t.state_ = 'completed'
  718 + AND DATE(t.END_TIME_) = CURDATE()
  719 + GROUP BY t.ASSIGNEE_
  720 + ) wo ON u.id = wo.user_id
  721 + WHERE u.dept_id = #{deptId}
  722 + AND u.deleted = 0
  723 + </select>
618 724  
619 725 </mapper>
... ...
urbanops-module-system/src/main/java/com/zteits/urbanops/module/system/enums/DictTypeConstants.java
... ... @@ -32,8 +32,8 @@ public interface DictTypeConstants {
32 32  
33 33 String SYSTEM_IS_INNER = "system_is_inner";//是否是内部员工
34 34  
  35 + String TRAVEL_SPEED_CONFIG = "travel_speed_config"; // 出行速度配置
35 36  
36   -
37   -
  37 + String TASK_SORT_WEIGHT_CONFIG = "task_sort_weight_config"; // 智能排序权重配置
38 38  
39 39 }
... ...
urbanops-server/src/main/resources/application.yaml
... ... @@ -271,7 +271,7 @@ urbanops:
271 271 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
272 272 security:
273 273 permit-all_urls:
274   - - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
  274 + - /admin-api/mp/open/**
275 275 - /admin-api/garden/device-user/**
276 276 api-encrypt:
277 277 enable: true # 是否开启 API 加密
... ...