Commit 61c9550cc2688cef09c76cb98fd01b22a1465692

Authored by 王彪总
2 parents b362df5b fa50f5a7

Merge remote-tracking branch 'origin/master' into dev

Showing 19 changed files with 1086 additions and 6 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
... ... @@ -12,6 +12,7 @@ import jakarta.validation.Valid;
12 12 import org.springframework.web.bind.annotation.*;
13 13  
14 14 import java.util.List;
  15 +import java.util.Map;
15 16  
16 17 import static com.zteits.urbanops.framework.common.pojo.CommonResult.success;
17 18  
... ... @@ -52,4 +53,16 @@ public class AppHomepageSummaryController {
52 53 public CommonResult<AppWorkOrderSummaryRspVo> iWorkOrderSummary(@Valid @RequestBody AppWorkOrderSummaryReqVo req) {
53 54 return success(homepageSummaryService.iWorkOrderSummary(req));
54 55 }
  56 +
  57 + @PostMapping("/taskDetailsWithDistance")
  58 + @Operation(summary = "任务待办(按距离排序 + 班组进度)")
  59 + public CommonResult<TaskDistanceSortResultVO> queryTaskDetailsWithDistance(@Valid @RequestBody AppTaskDistanceSortReqVO req) {
  60 + return success(homepageSummaryService.queryTaskDetailsWithDistance(req));
  61 + }
  62 +
  63 + @PostMapping("/taskDetailsWithSmartSort")
  64 + @Operation(summary = "任务待办(智能排序:距离 + 个人效率加权)")
  65 + public CommonResult<TaskDistanceSortResultVO> queryTaskDetailsWithSmartSort(@Valid @RequestBody AppTaskSmartSortReqVO req) {
  66 + return success(homepageSummaryService.queryTaskDetailsWithSmartSort(req));
  67 + }
55 68 }
... ...
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
... ... @@ -10,6 +10,7 @@ import com.zteits.urbanops.module.garden.controller.app.homepage.vo.AppWorkOrder
10 10 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.CommonTaskStatusVo;
11 11 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskCompletionStatusRspVo;
12 12 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDetailsRspVo;
  13 +import com.zteits.urbanops.module.garden.controller.app.homepage.vo.TaskDistanceSortRespVO;
13 14 import com.zteits.urbanops.module.garden.dal.dataobject.departmentcost.DepartmentCostDO;
14 15 import com.zteits.urbanops.module.garden.dal.dataobject.taskstatistics.TaskStatisticsDO;
15 16 import org.apache.ibatis.annotations.Mapper;
... ... @@ -216,4 +217,17 @@ public interface HomepageSummaryMapper extends BaseMapperX&lt;DepartmentCostDO&gt; {
216 217 * @Return
217 218 */
218 219 List<AppWorkOrderSummaryVo> countWorkOrder(@Param("queryType") String queryType,@Param("userId") Long userId, @Param("beginTime") String beginTime, @Param("endTime") String endTime);
  220 +
  221 + /**
  222 + * 查询今日待办任务(含 GPS 坐标),三表 UNION
  223 + */
  224 + List<TaskDistanceSortRespVO> queryPendingTaskDetailsWithGps(@Param("roleIds") List<Long> roleIds,
  225 + @Param("userId") Long userId,
  226 + @Param("deptId") Long deptId,
  227 + @Param("currentDate") LocalDateTime currentDate);
  228 +
  229 + /**
  230 + * 统计今日同班组(同 dept_id)成员完成任务数
  231 + */
  232 + List<Map<String, Object>> countTeamCompletedToday(@Param("deptId") Long deptId);
219 233 }
... ...
urbanops-module-garden/src/main/java/com/zteits/urbanops/module/garden/enums/CommonConstants.java
... ... @@ -26,4 +26,7 @@ public interface CommonConstants {
26 26  
27 27 /*积分来源 01:任务反馈*/
28 28 public static final String UNIT_INTEGRAL_SOURCE_TYPE_01 = "01";
  29 +
  30 + /*全域督察员角色标识*/
  31 + public static final String INSPECTOR_ROLE_KEY = "inspector";
29 32 }
... ...
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
... ... @@ -4,6 +4,7 @@ import com.zteits.urbanops.framework.common.pojo.PageResult;
4 4 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*;
5 5  
6 6 import java.util.List;
  7 +import java.util.Map;
7 8  
8 9 /**
9 10 * @Classname HomepageSummaryService
... ... @@ -48,4 +49,16 @@ public interface HomepageSummaryService {
48 49 AppWorkOrderSummaryRspVo iWorkOrderSummary(AppWorkOrderSummaryReqVo req);
49 50  
50 51  
  52 + /**
  53 + * 任务待办(按距离排序 + 班组进度)
  54 + *
  55 + * @param req 请求参数(当前 GPS 坐标)
  56 + * @return 排序后的任务列表 + 班组进度统计
  57 + */
  58 + TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req);
  59 +
  60 + /**
  61 + * 任务待办(智能排序:距离 + 个人效率加权)
  62 + */
  63 + TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req);
51 64 }
... ...
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;
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;
  13 +import com.zteits.urbanops.framework.common.biz.system.dict.dto.DictDataRespDTO;
11 14 import com.zteits.urbanops.module.garden.controller.app.homepage.vo.*;
  15 +import com.zteits.urbanops.module.garden.dal.dataobject.UserTaskEfficiencyDO;
  16 +import com.zteits.urbanops.module.garden.enums.CommonConstants;
12 17 import com.zteits.urbanops.module.garden.dal.dataobject.taskstatistics.TaskStatisticsDO;
  18 +import com.zteits.urbanops.module.garden.dal.mysql.UserTaskEfficiencyMapper;
13 19 import com.zteits.urbanops.module.garden.dal.mysql.homepage.HomepageSummaryMapper;
14 20 import com.zteits.urbanops.module.garden.dal.mysql.taskstatistics.TaskStatisticsMapper;
  21 +import com.zteits.urbanops.module.garden.util.GpsDistanceUtil;
15 22 import com.zteits.urbanops.module.system.api.dept.DeptApi;
  23 +import com.zteits.urbanops.module.system.api.dict.DictDataApi;
16 24 import com.zteits.urbanops.module.system.api.permission.RoleApi;
17 25 import com.zteits.urbanops.module.system.api.user.AdminUserApi;
18   -import com.zteits.urbanops.module.system.enums.common.CommonConstants;
  26 +import com.zteits.urbanops.module.system.api.user.dto.AdminUserRespDTO;
  27 +import com.zteits.urbanops.module.system.enums.DictTypeConstants;
19 28 import jakarta.annotation.Resource;
20 29 import org.apache.commons.collections4.CollectionUtils;
21 30 import org.apache.commons.lang3.StringUtils;
22 31 import org.springframework.stereotype.Service;
23 32 import org.springframework.transaction.annotation.Transactional;
24 33  
  34 +import java.math.BigDecimal;
25 35 import java.text.DateFormat;
26 36 import java.text.ParseException;
27 37 import java.text.SimpleDateFormat;
28 38 import java.time.LocalDateTime;
  39 +import java.time.LocalTime;
29 40 import java.time.format.DateTimeFormatter;
30 41 import java.util.*;
31 42 import java.util.stream.Collectors;
... ... @@ -58,6 +69,12 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
58 69 @Resource
59 70 private DeptApi deptApi;
60 71  
  72 + @Resource
  73 + private DictDataApi dictDataApi;
  74 +
  75 + @Resource
  76 + private UserTaskEfficiencyMapper userTaskEfficiencyMapper;
  77 +
61 78 @Override
62 79 public List<TaskCompletionStatusRspVo> taskCompletionSummary(AppHomePageSummaryReqVO req) {
63 80 Long userId = getLoginUserId();
... ... @@ -408,4 +425,360 @@ public class HomepageSummaryServiceImpl implements HomepageSummaryService{
408 425  
409 426 return days;
410 427 }
  428 +
  429 + @Override
  430 + public TaskDistanceSortResultVO queryTaskDetailsWithDistance(AppTaskDistanceSortReqVO req) {
  431 + // 1. 获取当前登录用户信息
  432 + Long userId = SecurityFrameworkUtils.getLoginUserId();
  433 + Long deptId = SecurityFrameworkUtils.getDeptId();
  434 + List<Long> roleIds = roleApi.getRoleIdsByUserId(userId);
  435 +
  436 + // 2. 根据 queryType 分流
  437 + List<TaskDistanceSortRespVO> sortedTasks;
  438 + if (req.getQueryType() != null && req.getQueryType() == 2) {
  439 + // ========== 已办:查询已完成任务(无 GPS 距离排序) ==========
  440 + sortedTasks = queryCompletedTasksWithoutGps(userId);
  441 + } else {
  442 + // ========== 待办:查询待办任务 + GPS 贪心最近邻排序 ==========
  443 + sortedTasks = queryPendingTasksWithGpsSort(req, userId, deptId, roleIds);
  444 + }
  445 +
  446 + // 3. 查询班组进度
  447 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId);
  448 +
  449 + // 4. 组装返回结果
  450 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO();
  451 + result.setSortedTasks(sortedTasks);
  452 + result.setTeamProgress(teamProgress);
  453 + result.setTotalPendingTasks(sortedTasks.size());
  454 + int totalDist = 0;
  455 + int totalWalkMin = 0;
  456 + int totalTriMin = 0;
  457 + for (TaskDistanceSortRespVO task : sortedTasks) {
  458 + if (task.getDistance() != null) {
  459 + totalDist += task.getDistance();
  460 + totalWalkMin += (task.getEstimatedWalkingMinutes() != null ? task.getEstimatedWalkingMinutes() : 0);
  461 + totalTriMin += (task.getEstimatedTricycleMinutes() != null ? task.getEstimatedTricycleMinutes() : 0);
  462 + }
  463 + }
  464 + result.setEstimatedTotalDistance(totalDist);
  465 + result.setEstimatedTotalWalkingMinutes(totalWalkMin);
  466 + result.setEstimatedTotalTricycleMinutes(totalTriMin);
  467 +
  468 + return result;
  469 + }
  470 +
  471 + /**
  472 + * 已办任务查询(无 GPS 排序)
  473 + */
  474 + private List<TaskDistanceSortRespVO> queryCompletedTasksWithoutGps(Long userId) {
  475 + IPage<TaskDetailsRspVo> page = MyBatisUtils.buildPage(new AppHomePageSummaryPageReqVO());
  476 + // 设置大分页以获取全部已办任务
  477 + page.setSize(Integer.MAX_VALUE);
  478 + IPage<TaskDetailsRspVo> mpPage = homepageSummaryMapper.queryCompletedTaskDetails(page, userId);
  479 + List<TaskDistanceSortRespVO> result = new ArrayList<>();
  480 + if (mpPage != null && CollectionUtils.isNotEmpty(mpPage.getRecords())) {
  481 + int i = 0;
  482 + for (TaskDetailsRspVo vo : mpPage.getRecords()) {
  483 + TaskDistanceSortRespVO sortVo = new TaskDistanceSortRespVO();
  484 + sortVo.setTaskName(vo.getTaskName());
  485 + sortVo.setBusiDateTime(vo.getBusiDateTime());
  486 + sortVo.setPressingType(vo.getPressingType());
  487 + sortVo.setSortOrder(++i);
  488 + sortVo.setDistance(null);
  489 + sortVo.setCumulativeDistance(null);
  490 + sortVo.setEstimatedWalkingMinutes(null);
  491 + sortVo.setEstimatedTricycleMinutes(null);
  492 + result.add(sortVo);
  493 + }
  494 + }
  495 + return result;
  496 + }
  497 +
  498 + /**
  499 + * 待办任务查询 + GPS 贪心最近邻排序
  500 + */
  501 + private List<TaskDistanceSortRespVO> queryPendingTasksWithGpsSort(AppTaskDistanceSortReqVO req,
  502 + Long userId, Long deptId,
  503 + List<Long> roleIds) {
  504 + // 从字典表读取速度配置
  505 + double walkingSpeedKmh = 4.0;
  506 + double tricycleSpeedKmh = 15.0;
  507 + try {
  508 + List<DictDataRespDTO> speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG);
  509 + if (CollectionUtils.isNotEmpty(speedConfigs)) {
  510 + for (DictDataRespDTO config : speedConfigs) {
  511 + if ("步行速度(km/h)".equals(config.getLabel())) {
  512 + walkingSpeedKmh = Double.parseDouble(config.getValue());
  513 + } else if ("三轮车速度(km/h)".equals(config.getLabel())) {
  514 + tricycleSpeedKmh = Double.parseDouble(config.getValue());
  515 + }
  516 + }
  517 + }
  518 + } catch (Exception e) {
  519 + Log.warn("读取出行速度字典配置失败,使用默认值", e);
  520 + }
  521 + double walkingSpeedMperMin = walkingSpeedKmh * 1000 / 60;
  522 + double tricycleSpeedMperMin = tricycleSpeedKmh * 1000 / 60;
  523 +
  524 + // 查询今日待办任务(含 GPS 坐标)
  525 + LocalDateTime now = LocalDateTime.now();
  526 + List<TaskDistanceSortRespVO> allTasks = homepageSummaryMapper
  527 + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, now);
  528 +
  529 + // 分离有 GPS 和无 GPS 任务
  530 + List<TaskDistanceSortRespVO> tasksWithGps = new ArrayList<>();
  531 + List<TaskDistanceSortRespVO> tasksWithoutGps = new ArrayList<>();
  532 + if (CollectionUtils.isNotEmpty(allTasks)) {
  533 + for (TaskDistanceSortRespVO task : allTasks) {
  534 + if (task.getTaskLat() != null && task.getTaskLon() != null) {
  535 + tasksWithGps.add(task);
  536 + } else {
  537 + tasksWithoutGps.add(task);
  538 + }
  539 + }
  540 + }
  541 +
  542 + // 贪心最近邻排序
  543 + List<TaskDistanceSortRespVO> sortedTasks = new ArrayList<>();
  544 + double currentLat = req.getCurrentLat().doubleValue();
  545 + double currentLon = req.getCurrentLon().doubleValue();
  546 + int cumulativeDistance = 0;
  547 + int sortOrder = 0;
  548 +
  549 + List<TaskDistanceSortRespVO> remaining = new ArrayList<>(tasksWithGps);
  550 + while (!remaining.isEmpty()) {
  551 + TaskDistanceSortRespVO nearest = null;
  552 + double minDistance = Double.MAX_VALUE;
  553 + int nearestIndex = -1;
  554 + for (int i = 0; i < remaining.size(); i++) {
  555 + TaskDistanceSortRespVO task = remaining.get(i);
  556 + double dist = GpsDistanceUtil.haversineDistance(
  557 + currentLat, currentLon,
  558 + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue());
  559 + if (dist < minDistance) {
  560 + minDistance = dist;
  561 + nearest = task;
  562 + nearestIndex = i;
  563 + }
  564 + }
  565 + sortOrder++;
  566 + int distMeters = (int) Math.round(minDistance);
  567 + cumulativeDistance += distMeters;
  568 + nearest.setSortOrder(sortOrder);
  569 + nearest.setDistance(distMeters);
  570 + nearest.setCumulativeDistance(cumulativeDistance);
  571 + nearest.setEstimatedWalkingMinutes((int) Math.ceil(distMeters / walkingSpeedMperMin));
  572 + nearest.setEstimatedTricycleMinutes((int) Math.ceil(distMeters / tricycleSpeedMperMin));
  573 + sortedTasks.add(nearest);
  574 +
  575 + currentLat = nearest.getTaskLat().doubleValue();
  576 + currentLon = nearest.getTaskLon().doubleValue();
  577 + remaining.remove(nearestIndex);
  578 + }
  579 +
  580 + // 无 GPS 任务追加到末尾
  581 + if (CollectionUtils.isNotEmpty(tasksWithoutGps)) {
  582 + for (TaskDistanceSortRespVO task : tasksWithoutGps) {
  583 + sortOrder++;
  584 + task.setSortOrder(sortOrder);
  585 + task.setDistance(null);
  586 + task.setCumulativeDistance(null);
  587 + task.setEstimatedWalkingMinutes(null);
  588 + task.setEstimatedTricycleMinutes(null);
  589 + sortedTasks.add(task);
  590 + }
  591 + }
  592 + return sortedTasks;
  593 + }
  594 +
  595 + /**
  596 + * 构建班组进度统计
  597 + */
  598 + private TeamProgressVO buildTeamProgress(Long userId, Long deptId) {
  599 + TeamProgressVO teamProgress = new TeamProgressVO();
  600 + teamProgress.setUserCompletedCount(0);
  601 + teamProgress.setTeamAverage(0.0);
  602 + teamProgress.setTeamMax(0);
  603 + teamProgress.setTeamMemberCount(0);
  604 + try {
  605 + List<Map<String, Object>> teamData = homepageSummaryMapper.countTeamCompletedToday(deptId);
  606 + if (CollectionUtils.isNotEmpty(teamData)) {
  607 + int totalCompleted = 0;
  608 + int maxCompleted = 0;
  609 + for (Map<String, Object> row : teamData) {
  610 + Object uidObj = row.get("userId");
  611 + Object cntObj = row.get("completedCount");
  612 + int count = cntObj != null ? ((Number) cntObj).intValue() : 0;
  613 + totalCompleted += count;
  614 + if (count > maxCompleted) {
  615 + maxCompleted = count;
  616 + }
  617 + if (uidObj != null && userId.equals(((Number) uidObj).longValue())) {
  618 + teamProgress.setUserCompletedCount(count);
  619 + }
  620 + }
  621 + int memberCount = teamData.size();
  622 + teamProgress.setTeamMemberCount(memberCount);
  623 + teamProgress.setTeamAverage(memberCount > 0
  624 + ? Math.round(totalCompleted * 10.0 / memberCount) / 10.0 : 0.0);
  625 + teamProgress.setTeamMax(maxCompleted);
  626 + }
  627 + } catch (Exception e) {
  628 + Log.warn("查询班组完成统计失败", e);
  629 + }
  630 + return teamProgress;
  631 + }
  632 +
  633 + @Override
  634 + public TaskDistanceSortResultVO queryTaskDetailsWithSmartSort(AppTaskSmartSortReqVO req) {
  635 + Long userId = SecurityFrameworkUtils.getLoginUserId();
  636 + Long deptId = SecurityFrameworkUtils.getDeptId();
  637 + List<Long> roleIds = roleApi.getRoleIdsByUserId(userId);
  638 +
  639 + // 1. 从字典读取配置
  640 + double distWeight = 0.6, effWeight = 0.4, threshold = 2000;
  641 + double clampMin = 0.3, clampMax = 3.0;
  642 + try {
  643 + List<DictDataRespDTO> configs = dictDataApi.getDictDataList(DictTypeConstants.TASK_SORT_WEIGHT_CONFIG);
  644 + if (CollectionUtils.isNotEmpty(configs)) {
  645 + for (DictDataRespDTO c : configs) {
  646 + if ("距离权重".equals(c.getLabel())) distWeight = Double.parseDouble(c.getValue());
  647 + else if ("效率权重".equals(c.getLabel())) effWeight = Double.parseDouble(c.getValue());
  648 + else if ("效率比上限".equals(c.getLabel())) clampMax = Double.parseDouble(c.getValue());
  649 + else if ("效率比下限".equals(c.getLabel())) clampMin = Double.parseDouble(c.getValue());
  650 + }
  651 + }
  652 + List<DictDataRespDTO> speedConfigs = dictDataApi.getDictDataList(DictTypeConstants.TRAVEL_SPEED_CONFIG);
  653 + if (CollectionUtils.isNotEmpty(speedConfigs)) {
  654 + for (DictDataRespDTO c : speedConfigs) {
  655 + if ("步行可达距离阈值(m)".equals(c.getLabel())) threshold = Double.parseDouble(c.getValue());
  656 + }
  657 + }
  658 + } catch (Exception e) {
  659 + Log.warn("读取智能排序配置失败", e);
  660 + }
  661 +
  662 + // 2. 查询今日待办任务
  663 + List<TaskDistanceSortRespVO> allTasks = homepageSummaryMapper
  664 + .queryPendingTaskDetailsWithGps(roleIds, userId, deptId, LocalDateTime.now());
  665 +
  666 + // 3. 查询效率矩阵
  667 + Map<String, BigDecimal> effMap = loadEfficiencyMap(userId);
  668 +
  669 + // 4. 为每个任务打分
  670 + List<TaskDistanceSortRespVO> tasksWithGps = new ArrayList<>();
  671 + List<TaskDistanceSortRespVO> tasksWithoutGps = new ArrayList<>();
  672 + if (CollectionUtils.isNotEmpty(allTasks)) {
  673 + for (TaskDistanceSortRespVO task : allTasks) {
  674 + if (task.getTaskLat() != null && task.getTaskLon() != null) {
  675 + double dist = GpsDistanceUtil.haversineDistance(
  676 + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(),
  677 + task.getTaskLat().doubleValue(), task.getTaskLon().doubleValue());
  678 + double distScore = Math.max(0, 1 - dist / threshold);
  679 + task.setDistanceScore(Math.round(distScore * 100.0) / 100.0);
  680 +
  681 + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType());
  682 + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax);
  683 + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0);
  684 +
  685 + task.setCompositeScore(Math.round((distWeight * distScore + effWeight * effScore) * 100.0) / 100.0);
  686 + tasksWithGps.add(task);
  687 + } else {
  688 + BigDecimal effRatio = lookupEfficiency(effMap, task.getTaskType(), task.getTaskSubType());
  689 + double effScore = clamp(effRatio.doubleValue(), clampMin, clampMax);
  690 + task.setDistanceScore(0.0);
  691 + task.setEfficiencyScore(Math.round(effScore * 100.0) / 100.0);
  692 + task.setCompositeScore(Math.round((effWeight * effScore) * 100.0) / 100.0);
  693 + tasksWithoutGps.add(task);
  694 + }
  695 + }
  696 + }
  697 +
  698 + // 5. 排序:有GPS按综合分降序,无GPS追加末尾按效率分降序
  699 + tasksWithGps.sort((a, b) -> Double.compare(
  700 + b.getCompositeScore() != null ? b.getCompositeScore() : 0,
  701 + a.getCompositeScore() != null ? a.getCompositeScore() : 0));
  702 + tasksWithoutGps.sort((a, b) -> Double.compare(
  703 + b.getEfficiencyScore() != null ? b.getEfficiencyScore() : 0,
  704 + a.getEfficiencyScore() != null ? a.getEfficiencyScore() : 0));
  705 +
  706 + List<TaskDistanceSortRespVO> sortedTasks = new ArrayList<>();
  707 + int order = 0;
  708 + for (TaskDistanceSortRespVO t : tasksWithGps) {
  709 + t.setSortOrder(++order);
  710 + int distMeters = (int) Math.round(GpsDistanceUtil.haversineDistance(
  711 + req.getCurrentLat().doubleValue(), req.getCurrentLon().doubleValue(),
  712 + t.getTaskLat().doubleValue(), t.getTaskLon().doubleValue()));
  713 + t.setDistance(distMeters);
  714 + sortedTasks.add(t);
  715 + }
  716 + for (TaskDistanceSortRespVO t : tasksWithoutGps) {
  717 + t.setSortOrder(++order);
  718 + t.setDistance(null);
  719 + sortedTasks.add(t);
  720 + }
  721 +
  722 + // 6. 班组进度
  723 + TeamProgressVO teamProgress = buildTeamProgress(userId, deptId);
  724 +
  725 + // 7. 组装结果
  726 + TaskDistanceSortResultVO result = new TaskDistanceSortResultVO();
  727 + result.setSortedTasks(sortedTasks);
  728 + result.setTeamProgress(teamProgress);
  729 + result.setTotalPendingTasks(sortedTasks.size());
  730 + int totalDist = 0;
  731 + for (TaskDistanceSortRespVO t : sortedTasks) {
  732 + if (t.getDistance() != null) totalDist += t.getDistance();
  733 + }
  734 + result.setEstimatedTotalDistance(totalDist);
  735 + return result;
  736 + }
  737 +
  738 + /**
  739 + * 加载当前用户当月效率矩阵
  740 + */
  741 + private Map<String, BigDecimal> loadEfficiencyMap(Long userId) {
  742 + Map<String, BigDecimal> map = new LinkedHashMap<>();
  743 + try {
  744 + String statPeriod = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
  745 + List<UserTaskEfficiencyDO> list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, statPeriod);
  746 + if (CollectionUtils.isEmpty(list)) {
  747 + // fallback: 查最近一个周期的数据
  748 + String latestPeriod = userTaskEfficiencyMapper.selectLatestPeriod(userId);
  749 + if (latestPeriod != null) {
  750 + list = userTaskEfficiencyMapper.selectByUserIdAndPeriod(userId, latestPeriod);
  751 + }
  752 + }
  753 + if (CollectionUtils.isNotEmpty(list)) {
  754 + for (UserTaskEfficiencyDO e : list) {
  755 + String key = e.getTaskCategory() + "_" + e.getTaskType();
  756 + map.put(key, e.getEfficiencyRatio() != null ? e.getEfficiencyRatio() : BigDecimal.ONE);
  757 + }
  758 + }
  759 + } catch (Exception e) {
  760 + Log.warn("加载效率矩阵失败", e);
  761 + }
  762 + return map;
  763 + }
  764 +
  765 + /**
  766 + * 从效率矩阵查找,先精确匹配再 fallback 大类
  767 + */
  768 + private BigDecimal lookupEfficiency(Map<String, BigDecimal> map, String category, String subType) {
  769 + // 精确匹配
  770 + String exactKey = category + "_" + subType;
  771 + if (map.containsKey(exactKey)) return map.get(exactKey);
  772 + // fallback: 大类汇总
  773 + String fallbackKey = category + "_ALL";
  774 + if (map.containsKey(fallbackKey)) return map.get(fallbackKey);
  775 + // 无数据:中性
  776 + return BigDecimal.ONE;
  777 + }
  778 +
  779 + private double clamp(double val, double min, double max) {
  780 + if (val > max) return max;
  781 + if (val < min) return min;
  782 + return val;
  783 + }
411 784 }
... ...
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 加密
... ...