Browse Source

fix(risk): 巡检任务详情补 items/itemCount/punchLocation,作业/人员/安全隐患修复

- MobileInspectionTaskVo 增加 punchLocation 字段
- RiskInspectionTaskServiceImpl.queryMobileDetail 补 items/itemCount/punchLocation
- RiskWorkReport 审核接口独立 RiskWorkReportReviewBo,字典值统一为中文(待审核/审核通过/审核不通过)
- RiskPersonnelController saveWithCerts 改物理删+insert 模式,绕过 @TableLogic 软删导致的 PRIMARY KEY 冲突
- RiskPersonnelCertMapper 新增 deleteByIdsPhysically 物理删除
- MapProxyController 三个端点绕过浏览器 CORS(search/reverse/geocode)
- application-dev.yml 加 risk.qqmap.key
yangjingjing 9 hours ago
parent
commit
30b9acb057

+ 1 - 1
pom.xml

@@ -53,7 +53,7 @@
         <!-- 插件版本 -->
         <!-- 插件版本 -->
         <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
         <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
         <maven-war-plugin.version>3.4.0</maven-war-plugin.version>
         <maven-war-plugin.version>3.4.0</maven-war-plugin.version>
-        <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
+        <maven-compiler-plugin.version>3.10.0</maven-compiler-plugin.version>
         <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
         <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
         <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
         <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
         <!-- 打包默认跳过测试 -->
         <!-- 打包默认跳过测试 -->

+ 5 - 0
ruoyi-admin/src/main/resources/application-dev.yml

@@ -270,3 +270,8 @@ justauth:
       client-id: 10**********6
       client-id: 10**********6
       client-secret: 1f7d08**********5b7**********29e
       client-secret: 1f7d08**********5b7**********29e
       redirect-uri: ${justauth.address}/social-callback?source=gitea
       redirect-uri: ${justauth.address}/social-callback?source=gitea
+
+# 腾讯地图 WebService API Key(用于 /risk/map/* 代理)
+risk:
+  qqmap:
+    key: 'I4OBZ-ULG6Q-JXZ5N-4HSCN-PE4AZ-3TFRU'

+ 2 - 0
ruoyi-common/ruoyi-common-mybatis/src/main/resources/common-mybatis.yml

@@ -4,6 +4,8 @@
 mybatis-plus:
 mybatis-plus:
   # 启动时是否检查 MyBatis XML 文件的存在,默认不检查
   # 启动时是否检查 MyBatis XML 文件的存在,默认不检查
   checkConfigLocation: false
   checkConfigLocation: false
+  # mapper xml 路径(双星通配跨目录,扫 BOOT-INF/lib/*.jar 内的 mapper/**/*.xml)
+  mapper-locations: classpath*:mapper/**/*Mapper.xml
   configuration:
   configuration:
     # 自动驼峰命名规则(camel case)映射
     # 自动驼峰命名规则(camel case)映射
     mapUnderscoreToCamelCase: true
     mapUnderscoreToCamelCase: true

+ 108 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/MapProxyController.java

@@ -0,0 +1,108 @@
+package org.dromara.risk.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Map;
+
+/**
+ * 腾讯地图 WebService API 代理(绕过浏览器 CORS 限制)
+ *  - /risk/map/search?keyword=...        POI 搜索
+ *  - /risk/map/reverse?lat=...&lng=...    逆地理(经纬度→地址)
+ *  - /risk/map/geocode?address=...       地理编码(地址→经纬度)
+ */
+@Slf4j
+@SaIgnore
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/risk/map")
+public class MapProxyController {
+
+    @Value("${risk.qqmap.key:${qqmap.key:}}")
+    private String qqmapKey;
+
+    private static final HttpClient HTTP = HttpClient.newBuilder()
+        .connectTimeout(Duration.ofSeconds(5))
+        .build();
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @GetMapping("/search")
+    public Map<String, Object> search(@RequestParam("keyword") String keyword,
+                                      @RequestParam(value = "region", required = false) String region) {
+        if (qqmapKey == null || qqmapKey.isBlank()) {
+            return Map.of("status", -1, "message", "后端未配置 risk.qqmap.key");
+        }
+        StringBuilder sb = new StringBuilder("https://apis.map.qq.com/ws/place/v1/search")
+            .append("?keyword=").append(enc(keyword))
+            .append("&page_size=5");
+        // 腾讯 place v1 强制要 boundary,不传就 348
+        if (region != null && !region.isBlank()) {
+            sb.append("&boundary=region(").append(enc(region)).append(",0)");
+        } else {
+            sb.append("&boundary=region(中国,1)");
+        }
+        sb.append("&key=").append(enc(qqmapKey));
+        return call(sb.toString(), "搜索");
+    }
+
+    @GetMapping("/reverse")
+    public Map<String, Object> reverse(@RequestParam("lat") Double lat, @RequestParam("lng") Double lng) {
+        if (qqmapKey == null || qqmapKey.isBlank()) {
+            return Map.of("status", -1, "message", "后端未配置 risk.qqmap.key");
+        }
+        String url = "https://apis.map.qq.com/ws/geocoder/v1/"
+            + "?location=" + lat + "," + lng
+            + "&get_poi=0"
+            + "&key=" + enc(qqmapKey);
+        return call(url, "逆地理");
+    }
+
+    @GetMapping("/geocode")
+    public Map<String, Object> geocode(@RequestParam("address") String address) {
+        if (qqmapKey == null || qqmapKey.isBlank()) {
+            return Map.of("status", -1, "message", "后端未配置 risk.qqmap.key");
+        }
+        String url = "https://apis.map.qq.com/ws/geocoder/v1/"
+            + "?address=" + enc(address)
+            + "&key=" + enc(qqmapKey);
+        return call(url, "地理编码");
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> call(String url, String action) {
+        log.info("[map-proxy] {} -> {}", action, url);
+        try {
+            HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+                .timeout(Duration.ofSeconds(8))
+                .header("User-Agent", "ruoyi-risk/5.6.2")
+                .GET()
+                .build();
+            HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+            if (resp.statusCode() != 200) {
+                return Map.of("status", resp.statusCode(), "message", action + "失败: HTTP " + resp.statusCode());
+            }
+            return MAPPER.readValue(resp.body(), Map.class);
+        } catch (Exception e) {
+            log.warn("腾讯地图 {} 失败: {}", action, e.getMessage());
+            return Map.of("status", -1, "message", action + "失败: " + e.getMessage());
+        }
+    }
+
+    private static String enc(String s) {
+        return URLEncoder.encode(s, StandardCharsets.UTF_8);
+    }
+}

+ 38 - 18
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskPersonnelController.java

@@ -19,6 +19,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.risk.domain.bo.RiskPersonnelBo;
 import org.dromara.risk.domain.bo.RiskPersonnelBo;
 import org.dromara.risk.domain.bo.RiskPersonnelCertBo;
 import org.dromara.risk.domain.bo.RiskPersonnelCertBo;
+import org.dromara.risk.domain.vo.RiskPersonnelCertVo;
 import org.dromara.risk.domain.vo.RiskPersonnelVo;
 import org.dromara.risk.domain.vo.RiskPersonnelVo;
 import org.dromara.risk.service.IRiskPersonnelService;
 import org.dromara.risk.service.IRiskPersonnelService;
 import org.dromara.risk.service.IRiskPersonnelCertService;
 import org.dromara.risk.service.IRiskPersonnelCertService;
@@ -104,6 +105,11 @@ public class RiskPersonnelController extends BaseController {
     /**
     /**
      * 保存人员信息(含证书列表),适用于新增和修改
      * 保存人员信息(含证书列表),适用于新增和修改
      * 请求体格式: { personnel: {...}, certList: [...] }
      * 请求体格式: { personnel: {...}, certList: [...] }
+     * <p>
+     * 修改分支:人员 update;该人员下所有 cert <b>物理删</b>(绕开 @TableLogic 软删) + 重新 insert。
+     * 这样既能保证 cert id 走 AUTO_INCREMENT(不冲突),又避免软删垃圾累积。
+     * <p>
+     * 前端 certBo <b>不传 id</b>(让后端 insert 走 AUTO_INCREMENT 分配新 id)。
      */
      */
     @SaCheckPermission("risk:personnel:add")
     @SaCheckPermission("risk:personnel:add")
     @Log(title = "人员信息", businessType = BusinessType.INSERT)
     @Log(title = "人员信息", businessType = BusinessType.INSERT)
@@ -117,20 +123,12 @@ public class RiskPersonnelController extends BaseController {
             java.util.Map<String, Object> map = (java.util.Map<String, Object>) payload.get("personnel");
             java.util.Map<String, Object> map = (java.util.Map<String, Object>) payload.get("personnel");
             BeanUtil.fillBeanWithMap(map, personnel, true);
             BeanUtil.fillBeanWithMap(map, personnel, true);
         }
         }
-        // 新增
-        if (personnel.getId() == null) {
-            personnelService.insertByBo(personnel);
-        } else {
-            // 修改
-            personnelService.updateByBo(personnel);
-            // 先删除该人员下的所有证书
-            certService.deleteByPersonnelId(personnel.getId());
-        }
-        // 批量插入证书
+
+        // 解析 certList(公共逻辑,新增/修改都用)
+        java.util.List<RiskPersonnelCertBo> certBos = new java.util.ArrayList<>();
         Object certObj = payload.get("certList");
         Object certObj = payload.get("certList");
         if (certObj instanceof java.util.List) {
         if (certObj instanceof java.util.List) {
             java.util.List<?> rawList = (java.util.List<?>) certObj;
             java.util.List<?> rawList = (java.util.List<?>) certObj;
-            java.util.List<RiskPersonnelCertBo> certBos = new java.util.ArrayList<>();
             for (Object o : rawList) {
             for (Object o : rawList) {
                 if (o instanceof java.util.Map) {
                 if (o instanceof java.util.Map) {
                     RiskPersonnelCertBo bo = org.springframework.beans.BeanUtils
                     RiskPersonnelCertBo bo = org.springframework.beans.BeanUtils
@@ -138,20 +136,42 @@ public class RiskPersonnelController extends BaseController {
                     @SuppressWarnings("unchecked")
                     @SuppressWarnings("unchecked")
                     java.util.Map<String, Object> m = (java.util.Map<String, Object>) o;
                     java.util.Map<String, Object> m = (java.util.Map<String, Object>) o;
                     BeanUtil.fillBeanWithMap(m, bo, true);
                     BeanUtil.fillBeanWithMap(m, bo, true);
+                    // 清空 id,强制走 AUTO_INCREMENT(避免与软删残留主键冲突)
+                    bo.setId(null);
                     // certName 必填,缺失时回退到 certType,再缺失则用默认名
                     // certName 必填,缺失时回退到 certType,再缺失则用默认名
                     if (cn.hutool.core.util.StrUtil.isBlank(bo.getCertName())) {
                     if (cn.hutool.core.util.StrUtil.isBlank(bo.getCertName())) {
-                        if (cn.hutool.core.util.StrUtil.isNotBlank(bo.getCertType())) {
-                            bo.setCertName(bo.getCertType());
-                        } else {
-                            bo.setCertName("岗位证书");
-                        }
+                        bo.setCertName(cn.hutool.core.util.StrUtil.isNotBlank(bo.getCertType())
+                            ? bo.getCertType() : "岗位证书");
                     }
                     }
                     certBos.add(bo);
                     certBos.add(bo);
                 }
                 }
             }
             }
-            if (!certBos.isEmpty()) {
-                certService.insertBatchByBo(personnel.getId(), certBos);
+        }
+
+        // 区分新增/修改(用前端传入的 id 是否为空判断)
+        boolean isUpdate = personnel.getId() != null;
+
+        if (isUpdate) {
+            // 修改:人员 update
+            personnelService.updateByBo(personnel);
+            // 物理删该人员下所有 cert(绕开 @TableLogic 软删,避免主键残留)
+            java.util.List<RiskPersonnelCertVo> existing =
+                certService.queryListByPersonnelId(personnel.getId());
+            java.util.List<Long> existingIds = new java.util.ArrayList<>();
+            for (RiskPersonnelCertVo e : existing) {
+                if (e.getId() != null) existingIds.add(e.getId());
+            }
+            if (!existingIds.isEmpty()) {
+                certService.deleteByIdsPhysically(existingIds);
             }
             }
+        } else {
+            // 新增
+            personnelService.insertByBo(personnel);
+        }
+
+        // 插入 cert(走 AUTO_INCREMENT,无论新增/修改都这么干)
+        if (!certBos.isEmpty()) {
+            certService.insertBatchByBo(personnel.getId(), certBos);
         }
         }
         return R.ok(personnel.getId());
         return R.ok(personnel.getId());
     }
     }

+ 2 - 1
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskWorkReportController.java

@@ -19,6 +19,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.satoken.utils.LoginHelper;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.common.web.core.BaseController;
 import org.dromara.risk.domain.bo.RiskWorkReportBo;
 import org.dromara.risk.domain.bo.RiskWorkReportBo;
+import org.dromara.risk.domain.bo.RiskWorkReportReviewBo;
 import org.dromara.risk.domain.vo.RiskWorkReportVo;
 import org.dromara.risk.domain.vo.RiskWorkReportVo;
 import org.dromara.risk.service.IRiskWorkReportService;
 import org.dromara.risk.service.IRiskWorkReportService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
@@ -105,7 +106,7 @@ public class RiskWorkReportController extends BaseController {
     @Log(title = "作业上报", businessType = BusinessType.UPDATE)
     @Log(title = "作业上报", businessType = BusinessType.UPDATE)
     @RepeatSubmit()
     @RepeatSubmit()
     @PutMapping("/review")
     @PutMapping("/review")
-    public R<Void> review(@Validated @RequestBody RiskWorkReportBo bo) {
+    public R<Void> review(@Validated @RequestBody RiskWorkReportReviewBo bo) {
         // 前端未传审核人时,自动取当前登录用户
         // 前端未传审核人时,自动取当前登录用户
         String reviewer = StringUtils.isBlank(bo.getAuditorName())
         String reviewer = StringUtils.isBlank(bo.getAuditorName())
             ? LoginHelper.getUsername()
             ? LoginHelper.getUsername()

+ 44 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskWorkReportReviewBo.java

@@ -0,0 +1,44 @@
+package org.dromara.risk.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 作业上报审核 入参
+ * <p>
+ * 与新增/编辑 BO 解耦,只校验审核相关字段,避免 workType 等必填校验干扰审核接口
+ *
+ * @author mavis
+ */
+@Data
+public class RiskWorkReportReviewBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @NotNull(message = "作业上报ID不能为空")
+    private Long id;
+
+    /**
+     * 审核结果(待审核 / 审核通过 / 审核不通过)
+     */
+    @NotBlank(message = "审核结果不能为空")
+    private String auditStatus;
+
+    /**
+     * 审核人(为空时后端取当前登录用户)
+     */
+    private String auditorName;
+
+    /**
+     * 审核意见
+     */
+    private String auditOpinion;
+}

+ 7 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/MobileInspectionTaskVo.java

@@ -88,6 +88,13 @@ public class MobileInspectionTaskVo implements Serializable {
     /** 已完成数 */
     /** 已完成数 */
     private Integer completedCount;
     private Integer completedCount;
 
 
+    /**
+     * 打卡地点(已打卡时=实际打卡位置,未打卡时=任务预设位置)
+     * - 优先取最新一条 record.check_in_location
+     * - 没打卡则取 task.location(任务预设)
+     */
+    private String punchLocation;
+
     /** 巡检项目列表(详情页用,前端 InspectionItem 字段:itemCategory/itemContent/itemStandard/needPhoto/result/...) */
     /** 巡检项目列表(详情页用,前端 InspectionItem 字段:itemCategory/itemContent/itemStandard/needPhoto/result/...) */
     private List<RiskInspectionTaskItemVo> items;
     private List<RiskInspectionTaskItemVo> items;
 }
 }

+ 19 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/mapper/RiskPersonnelCertMapper.java

@@ -3,6 +3,7 @@ package org.dromara.risk.mapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
+import org.apache.ibatis.annotations.Delete;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
 import org.dromara.common.mybatis.annotation.DataColumn;
 import org.dromara.common.mybatis.annotation.DataColumn;
 import org.dromara.common.mybatis.annotation.DataPermission;
 import org.dromara.common.mybatis.annotation.DataPermission;
@@ -75,4 +76,22 @@ public interface RiskPersonnelCertMapper extends BaseMapperPlus<RiskPersonnelCer
         @DataColumn(key = "userName", value = "create_by")
         @DataColumn(key = "userName", value = "create_by")
     })
     })
     int updateById(@Param(Constants.ENTITY) RiskPersonnelCert entity);
     int updateById(@Param(Constants.ENTITY) RiskPersonnelCert entity);
+
+    /**
+     * 物理删除(绕过 @TableLogic 软删)
+     * <p>
+     * 用于编辑人员时,清除"被前端移除"的证书,避免软删累积垃圾数据
+     *
+     * @param ids 证书主键集合
+     * @return 影响行数
+     */
+    @Delete({
+        "<script>",
+        "DELETE FROM risk_personnel_cert WHERE id IN ",
+        "<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
+        "#{id}",
+        "</foreach>",
+        "</script>"
+    })
+    int deleteByIdsPhysically(@Param("ids") List<Long> ids);
 }
 }

+ 8 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskPersonnelCertService.java

@@ -88,4 +88,12 @@ public interface IRiskPersonnelCertService {
      */
      */
     int deleteByPersonnelId(Long personnelId);
     int deleteByPersonnelId(Long personnelId);
 
 
+    /**
+     * 物理删除(绕过 @TableLogic 软删)
+     *
+     * @param ids 证书主键集合
+     * @return 影响行数
+     */
+    int deleteByIdsPhysically(List<Long> ids);
+
 }
 }

+ 16 - 1
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskInspectionTaskServiceImpl.java

@@ -366,7 +366,22 @@ public class RiskInspectionTaskServiceImpl implements IRiskInspectionTaskService
         Assert.notNull(task, "巡检任务不存在: " + id);
         Assert.notNull(task, "巡检任务不存在: " + id);
         MobileInspectionTaskVo vo = convertToMobileVo(task);
         MobileInspectionTaskVo vo = convertToMobileVo(task);
         // 加载巡检项目列表(供详情页渲染;needPhoto 字符串"0"/"1" 留到前端按需转 boolean)
         // 加载巡检项目列表(供详情页渲染;needPhoto 字符串"0"/"1" 留到前端按需转 boolean)
-        vo.setItems(taskItemMapper.selectVoListByTaskId(id));
+        List<RiskInspectionTaskItemVo> items = taskItemMapper.selectVoListByTaskId(id);
+        vo.setItems(items);
+        vo.setItemCount(items == null ? 0 : items.size());
+
+        // 打卡地点:已打卡任务取实际打卡位置(record.check_in_location),未打卡则用任务预设位置
+        RiskInspectionRecord latest = recordMapper.selectOne(
+            Wrappers.<RiskInspectionRecord>lambdaQuery()
+                .eq(RiskInspectionRecord::getTaskId, id)
+                .orderByDesc(RiskInspectionRecord::getCheckInTime)
+                .last("LIMIT 1")
+        );
+        if (latest != null && latest.getCheckInLocation() != null && !latest.getCheckInLocation().isEmpty()) {
+            vo.setPunchLocation(latest.getCheckInLocation());
+        } else {
+            vo.setPunchLocation(vo.getLocation());
+        }
         return vo;
         return vo;
     }
     }
 
 

+ 11 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskPersonnelCertServiceImpl.java

@@ -147,5 +147,16 @@ public class RiskPersonnelCertServiceImpl implements IRiskPersonnelCertService {
             .eq(RiskPersonnelCert::getPersonnelId, personnelId));
             .eq(RiskPersonnelCert::getPersonnelId, personnelId));
     }
     }
 
 
+    /**
+     * 物理删除(绕过 @TableLogic 软删,真正从表里抹掉)
+     */
+    @Override
+    public int deleteByIdsPhysically(List<Long> ids) {
+        if (ObjectUtil.isEmpty(ids)) {
+            return 0;
+        }
+        return baseMapper.deleteByIdsPhysically(ids);
+    }
+
 
 
 }
 }