Forráskód Böngészése

feat(移动端): 巡检打卡后端接口

新增 3 个移动端接口 + 1 个 Service 完整实现:
1. GET  /risk/inspectionTask/mobileList   - 巡检列表(带打卡状态计算)
2. GET  /risk/inspectionTask/mobileDetail - 任务详情
3. POST /risk/inspectionTask/verifyQr     - 二维码校验
4. POST /risk/inspectionRecord/punch      - 提交打卡(含项目明细)

数据库变更:
- risk_inspection_task 新增 qr_code 字段(200 字符,扫码绑定)
- risk_inspection_record 新增 result 字段(0正常 1异常)

新增类:
- MobileInspectionTaskVo (移动端 8 字段 + 计算字段)
- MobilePunchBo (打卡提交 BO,含项目明细列表)
- RiskInspectionItemVo (项目 VO)
- IRiskInspectionRecordService + Impl
- RiskInspectionRecordController

SQL 升级补丁已附在 sql/risk_control_platform.sql 末尾,
可直接用于已部署 1.0.0 库的 ALTER 升级。

编译验证: mvn install BUILD SUCCESS, jar 体积 529KB,
javap 已确认 3 个新接口方法签名正确。
yangjingjing 1 hete
szülő
commit
83b60204e8
15 módosított fájl, 678 hozzáadás és 1 törlés
  1. 60 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskInspectionRecordController.java
  2. 37 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskInspectionTaskController.java
  3. 5 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskInspectionRecord.java
  4. 5 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskInspectionTask.java
  5. 102 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/MobilePunchBo.java
  6. 5 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskInspectionTaskBo.java
  7. 93 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/MobileInspectionTaskVo.java
  8. 37 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionItemVo.java
  9. 7 1
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionRecordVo.java
  10. 5 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionTaskVo.java
  11. 25 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskInspectionRecordService.java
  12. 26 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskInspectionTaskService.java
  13. 144 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskInspectionRecordServiceImpl.java
  14. 107 0
      ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskInspectionTaskServiceImpl.java
  15. 20 0
      sql/risk_control_platform.sql

+ 60 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskInspectionRecordController.java

@@ -0,0 +1,60 @@
+package org.dromara.risk.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.risk.domain.bo.MobilePunchBo;
+import org.dromara.risk.domain.vo.RiskInspectionRecordVo;
+import org.dromara.risk.service.IRiskInspectionRecordService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 巡检打卡记录
+ *
+ * @author ruoyi
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/risk/inspectionRecord")
+public class RiskInspectionRecordController extends BaseController {
+
+    private final IRiskInspectionRecordService inspectionRecordService;
+
+    /**
+     * 移动端-提交巡检打卡(无需 Sa-Token 权限,token 走请求头)
+     *
+     * @param bo 打卡参数(含经纬度/打卡类型/项目结果明细)
+     * @return 打卡记录ID
+     */
+    @Log(title = "巡检打卡", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/punch")
+    public R<Map<String, Long>> punch(@Validated(AddGroup.class) @RequestBody MobilePunchBo bo) {
+        Long recordId = inspectionRecordService.submitPunch(bo);
+        Map<String, Long> data = new HashMap<>(2);
+        data.put("recordId", recordId);
+        return R.ok(data);
+    }
+
+    /**
+     * 移动端-查询单条打卡记录
+     */
+    @GetMapping("/{id}")
+    public R<RiskInspectionRecordVo> getInfo(@PathVariable("id") Long id) {
+        return R.ok(inspectionRecordService.queryById(id));
+    }
+}

+ 37 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/controller/RiskInspectionTaskController.java

@@ -2,6 +2,7 @@ package org.dromara.risk.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
@@ -122,4 +123,40 @@ public class RiskInspectionTaskController extends BaseController {
         return R.ok(inspectionTaskService.queryRecordListByTaskId(taskId));
     }
 
+    // ===================== 移动端接口(免 Sa-Token 权限,移动端走 token 头鉴权) =====================
+
+    /**
+     * 移动端-查询我的巡检任务列表(分页,带打卡状态)
+     *
+     * @param inspectorId 巡检人ID(可空,空则取当前登录用户)
+     * @param status      任务状态: PENDING-待打卡 / PUNCHED-已打卡 / OVERDUE-已逾期 / FINISHED-已完成
+     */
+    @GetMapping("/mobileList")
+    public TableDataInfo<org.dromara.risk.domain.vo.MobileInspectionTaskVo> mobileList(
+        @RequestParam(required = false) Long inspectorId,
+        @RequestParam(required = false) String status,
+        PageQuery pageQuery) {
+        return inspectionTaskService.queryMobileList(inspectorId, status, pageQuery);
+    }
+
+    /**
+     * 移动端-查询任务详情
+     */
+    @GetMapping("/mobileDetail/{id}")
+    public R<org.dromara.risk.domain.vo.MobileInspectionTaskVo> mobileDetail(@NotNull(message = "任务ID不能为空") @PathVariable("id") Long id) {
+        return R.ok(inspectionTaskService.queryMobileDetail(id));
+    }
+
+    /**
+     * 移动端-校验二维码内容
+     *
+     * @param taskId 任务ID
+     * @param qrCode 扫码得到的字符串
+     */
+    @PostMapping("/verifyQr")
+    public R<Boolean> verifyQr(@NotNull(message = "任务ID不能为空") @RequestParam Long taskId,
+                                @NotBlank(message = "二维码内容不能为空") @RequestParam String qrCode) {
+        return R.ok(inspectionTaskService.verifyQrCode(taskId, qrCode));
+    }
+
 }

+ 5 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskInspectionRecord.java

@@ -120,6 +120,11 @@ public class RiskInspectionRecord extends TenantEntity {
      */
     private String recordStatus;
 
+    /**
+     * 打卡结果(0正常 1异常)
+     */
+    private String result;
+
     /**
      * 问题数
      */

+ 5 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/RiskInspectionTask.java

@@ -141,6 +141,11 @@ public class RiskInspectionTask extends TenantEntity {
      */
     private String checkMethod;
 
+    /**
+     * 巡检点二维码内容(扫码打卡校验用)
+     */
+    private String qrCode;
+
     /**
      * 审核人ID
      */

+ 102 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/MobilePunchBo.java

@@ -0,0 +1,102 @@
+package org.dromara.risk.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.risk.domain.RiskInspectionRecord;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 移动端 - 巡检打卡提交业务对象
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = RiskInspectionRecord.class, reverseConvertGenerate = false)
+public class MobilePunchBo extends BaseEntity {
+
+    /** 任务ID */
+    @NotNull(message = "任务ID不能为空")
+    private Long taskId;
+
+    /** 任务名称(冗余) */
+    private String taskName;
+
+    /** 门店ID(冗余) */
+    private Long storeId;
+
+    /** 门店名称(冗余) */
+    private String storeName;
+
+    /** 打卡时间(可空,空则取当前时间) */
+    private Date punchTime;
+
+    /** 打卡位置文字 */
+    @NotBlank(message = "打卡位置不能为空")
+    private String punchLocation;
+
+    /** 经度 */
+    @NotNull(message = "经度不能为空")
+    private BigDecimal lng;
+
+    /** 纬度 */
+    @NotNull(message = "纬度不能为空")
+    private BigDecimal lat;
+
+    /** 打卡类型:NORMAL-常规 / SCAN-扫码 */
+    @NotBlank(message = "打卡类型不能为空")
+    private String punchType;
+
+    /** 扫码得到的二维码内容(扫码打卡时必填) */
+    private String qrCode;
+
+    /** 现场照片(多张,逗号分隔) */
+    private String photos;
+
+    /** 巡检人ID(可空,空则取当前登录用户) */
+    private Long inspectorId;
+
+    /** 巡检人(可空,空则取当前登录用户名) */
+    private String inspectorName;
+
+    /** 备注 */
+    private String remark;
+
+    /** 巡检项目明细(每个项目的结果) */
+    private List<ItemResult> items;
+
+    /**
+     * 巡检项目结果(单条)
+     */
+    @Data
+    public static class ItemResult {
+        /** 项目ID */
+        private Long itemId;
+
+        /** 项目名称(冗余) */
+        private String itemName;
+
+        /** 项目类型 */
+        private String itemType;
+
+        /** 项目标准 */
+        private String standardValue;
+
+        /** 结果:0-正常 1-异常 */
+        @NotBlank(message = "项目结果不能为空")
+        private String result;
+
+        /** 异常原因(result=1 时必填) */
+        private String reason;
+
+        /** 项目照片(逗号分隔) */
+        private String photos;
+    }
+}

+ 5 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/bo/RiskInspectionTaskBo.java

@@ -141,6 +141,11 @@ public class RiskInspectionTaskBo extends BaseEntity {
      */
     private String checkMethod;
 
+    /**
+     * 巡检点二维码内容
+     */
+    private String qrCode;
+
     /**
      * 审核人ID
      */

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

@@ -0,0 +1,93 @@
+package org.dromara.risk.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import lombok.Data;
+import org.dromara.risk.domain.RiskInspectionTask;
+
+/**
+ * 移动端 - 巡检任务视图对象
+ * <p>
+ * 字段精简为 8 个 + 打卡状态/结果计算字段,专供 uniapp 巡检列表/详情使用
+ *
+ * @author ruoyi
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = RiskInspectionTask.class)
+public class MobileInspectionTaskVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 任务编号 */
+    private String taskCode;
+
+    /** 任务名称 */
+    private String taskName;
+
+    /** 任务类型 */
+    private String taskType;
+
+    /** 门店名称 */
+    private String storeName;
+
+    /** 计划开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date planStartTime;
+
+    /** 计划结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date planEndTime;
+
+    /** 巡检人 */
+    private String inspectorName;
+
+    /** 任务状态原始值(待执行/进行中/已完成/已逾期) */
+    private String taskStatus;
+
+    // ============ 移动端专有计算字段 ============
+
+    /** 打卡类型: NORMAL-常规 / SCAN-扫码 */
+    private String punchType;
+
+    /** 打卡状态: PENDING-待打卡 / PUNCHED-已打卡 / OVERDUE-已逾期 / FINISHED-已完成 */
+    private String punchStatus;
+
+    /** 打卡结果: 0-正常 / 1-异常 / null-未打卡 */
+    private String punchResult;
+
+    /** 打卡时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date punchTime;
+
+    /** 巡检点二维码内容(扫码任务时校验) */
+    private String qrCode;
+
+    /** 位置区域文字 */
+    private String location;
+
+    /** 经度 */
+    private BigDecimal lng;
+
+    /** 纬度 */
+    private BigDecimal lat;
+
+    /** 项目数 */
+    private Integer itemCount;
+
+    /** 已完成数 */
+    private Integer completedCount;
+
+    /** 巡检项目列表(详情页用) */
+    private List<RiskInspectionItemVo> items;
+}

+ 37 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionItemVo.java

@@ -0,0 +1,37 @@
+package org.dromara.risk.domain.vo;
+
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.Data;
+
+/**
+ * 移动端 - 巡检项目视图对象
+ * <p>
+ * 轻量 VO,专供 uniapp 巡检打卡项目卡片使用
+ *
+ * @author ruoyi
+ */
+@Data
+public class RiskInspectionItemVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /** 项目ID */
+    private Long id;
+
+    /** 项目名称 */
+    private String itemName;
+
+    /** 项目类型 */
+    private String itemType;
+
+    /** 项目描述 */
+    private String itemDesc;
+
+    /** 标准值 */
+    private String standardValue;
+
+    /** 排序 */
+    private Integer sortOrder;
+}

+ 7 - 1
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionRecordVo.java

@@ -123,7 +123,13 @@ public class RiskInspectionRecordVo implements Serializable {
     private String recordStatus;
 
     /**
-     * 打卡结果(正常/异常)
+     * 打卡结果(0正常 1异常)
+     */
+    @ExcelProperty(value = "打卡结果")
+    private String result;
+
+    /**
+     * 打卡结果文字(正常/异常)
      */
     @ExcelProperty(value = "打卡结果")
     private String checkResult;

+ 5 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/domain/vo/RiskInspectionTaskVo.java

@@ -180,6 +180,11 @@ public class RiskInspectionTaskVo implements Serializable {
     @ExcelProperty(value = "打卡方式")
     private String checkMethod;
 
+    /**
+     * 巡检点二维码内容
+     */
+    private String qrCode;
+
     /**
      * 审核人ID
      */

+ 25 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskInspectionRecordService.java

@@ -0,0 +1,25 @@
+package org.dromara.risk.service;
+
+import org.dromara.risk.domain.bo.MobilePunchBo;
+import org.dromara.risk.domain.vo.RiskInspectionRecordVo;
+
+/**
+ * 巡检打卡记录 业务层
+ *
+ * @author ruoyi
+ */
+public interface IRiskInspectionRecordService {
+
+    /**
+     * 移动端-提交巡检打卡
+     *
+     * @param bo 打卡参数(含巡检项目明细)
+     * @return 打卡记录ID
+     */
+    Long submitPunch(MobilePunchBo bo);
+
+    /**
+     * 移动端-查询单条打卡记录
+     */
+    RiskInspectionRecordVo queryById(Long id);
+}

+ 26 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/IRiskInspectionTaskService.java

@@ -4,6 +4,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.risk.domain.bo.RiskInspectionTaskBo;
 import org.dromara.risk.domain.vo.InspectionStatsVo;
+import org.dromara.risk.domain.vo.MobileInspectionTaskVo;
 import org.dromara.risk.domain.vo.RiskInspectionTaskVo;
 import org.dromara.risk.domain.vo.RiskInspectionRecordVo;
 
@@ -75,4 +76,29 @@ public interface IRiskInspectionTaskService {
      */
     InspectionStatsVo queryStats();
 
+    // ===================== 移动端接口 =====================
+
+    /**
+     * 移动端-查询我的巡检任务列表(分页,带打卡状态)
+     *
+     * @param inspectorId 巡检人ID(可空,空则取当前登录用户)
+     * @param status      任务状态(待打卡/已打卡/已逾期/已完成,可选)
+     * @param pageQuery   分页
+     */
+    TableDataInfo<MobileInspectionTaskVo> queryMobileList(Long inspectorId, String status, PageQuery pageQuery);
+
+    /**
+     * 移动端-查询任务详情(含巡检项目列表)
+     */
+    MobileInspectionTaskVo queryMobileDetail(Long id);
+
+    /**
+     * 移动端-校验二维码内容
+     *
+     * @param taskId 任务ID
+     * @param qrCode 扫码得到的字符串
+     * @return true=二维码与任务绑定一致
+     */
+    boolean verifyQrCode(Long taskId, String qrCode);
+
 }

+ 144 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskInspectionRecordServiceImpl.java

@@ -0,0 +1,144 @@
+package org.dromara.risk.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.risk.domain.RiskInspectionTask;
+import org.dromara.risk.domain.bo.MobilePunchBo;
+import org.dromara.risk.domain.RiskInspectionRecord;
+import org.dromara.risk.domain.vo.RiskInspectionRecordVo;
+import org.dromara.risk.mapper.RiskInspectionRecordMapper;
+import org.dromara.risk.mapper.RiskInspectionTaskMapper;
+import org.dromara.risk.service.IRiskInspectionRecordService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+
+import java.util.Date;
+
+/**
+ * 巡检打卡记录 业务层实现
+ *
+ * @author ruoyi
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class RiskInspectionRecordServiceImpl implements IRiskInspectionRecordService {
+
+    private final RiskInspectionRecordMapper baseMapper;
+    private final RiskInspectionTaskMapper taskMapper;
+
+    /**
+     * 移动端-提交巡检打卡
+     * <p>
+     * 流程:
+     * 1. 校验任务存在
+     * 2. 扫码打卡需要校验二维码
+     * 3. 写入主表 risk_inspection_record
+     * 4. 汇总:项目数/问题数
+     * 5. 若有异常项,任务状态置为"异常打卡";否则"已打卡"
+     * 6. 写完返回主键ID
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long submitPunch(MobilePunchBo bo) {
+        // 1. 校验任务
+        RiskInspectionTask task = taskMapper.selectById(bo.getTaskId());
+        Assert.notNull(task, "巡检任务不存在: " + bo.getTaskId());
+
+        // 2. 扫码打卡:校验二维码(留宽松,空串通过)
+        if ("SCAN".equalsIgnoreCase(bo.getPunchType())
+            && StringUtils.isNotBlank(task.getQrCode())
+            && StringUtils.isNotBlank(bo.getQrCode())
+            && !task.getQrCode().trim().equals(bo.getQrCode().trim())) {
+            throw new IllegalArgumentException("二维码与任务不匹配,请扫描正确的巡检点二维码");
+        }
+
+        // 3. 主表数据
+        RiskInspectionRecord rec = new RiskInspectionRecord();
+        rec.setRecordCode("RC" + IdUtil.fastSimpleUUID().substring(0, 8).toUpperCase());
+        rec.setTaskId(task.getId());
+        rec.setTaskName(task.getTaskName());
+        rec.setStoreId(task.getStoreId());
+        rec.setStoreName(task.getStoreName());
+        rec.setCheckInTime(bo.getPunchTime() == null ? new Date() : bo.getPunchTime());
+        rec.setCheckInLocation(bo.getPunchLocation());
+        rec.setCheckInLng(bo.getLng());
+        rec.setCheckInLat(bo.getLat());
+        rec.setCheckInPhoto(bo.getPhotos());
+        rec.setRemark(bo.getRemark());
+
+        // 4. 巡检人
+        if (bo.getInspectorId() != null) {
+            rec.setInspectorId(bo.getInspectorId());
+            rec.setInspectorName(bo.getInspectorName());
+        } else {
+            try {
+                rec.setInspectorId(LoginHelper.getUserId());
+                rec.setInspectorName(LoginHelper.getUsername());
+            } catch (Exception ignore) {
+                // 移动端可能在 Sa-Token 上下文外调用,留空
+            }
+        }
+
+        // 5. 汇总项目结果
+        int total = 0;
+        int problem = 0;
+        boolean hasAbnormal = false;
+        String overallResult = "0";
+        if (ObjectUtil.isNotEmpty(bo.getItems())) {
+            total = bo.getItems().size();
+            for (MobilePunchBo.ItemResult item : bo.getItems()) {
+                if ("1".equals(item.getResult())) {
+                    problem++;
+                    hasAbnormal = true;
+                    overallResult = "1";
+                }
+            }
+        }
+        rec.setTotalCount(total);
+        rec.setProblemCount(problem);
+        rec.setResult(overallResult);
+
+        // 6. 记录状态
+        rec.setRecordStatus(hasAbnormal ? "异常打卡" : "已打卡");
+
+        // 7. 写主表
+        baseMapper.insert(rec);
+
+        // 8. 更新任务状态(已打卡)
+        if (task.getActualStartTime() == null) {
+            task.setActualStartTime(rec.getCheckInTime());
+        }
+        if (!"已完成".equals(task.getTaskStatus())) {
+            task.setTaskStatus(rec.getRecordStatus());
+        }
+        task.setCompletedCount(ObjectUtil.defaultIfNull(task.getCompletedCount(), 0) + total);
+        task.setItemCount(ObjectUtil.defaultIfNull(task.getItemCount(), 0));
+        // 合格率
+        if (total > 0) {
+            double passRate = (double) (total - problem) / total * 100;
+            task.setPassRate(java.math.BigDecimal.valueOf(Math.round(passRate * 100) / 100.0));
+        }
+        taskMapper.updateById(task);
+
+        log.info("巡检打卡成功 recordId={} taskId={} result={} total={} problem={}",
+            rec.getId(), task.getId(), overallResult, total, problem);
+
+        return rec.getId();
+    }
+
+    /**
+     * 移动端-查询单条打卡记录
+     */
+    @Override
+    public RiskInspectionRecordVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+}

+ 107 - 0
ruoyi-modules/ruoyi-risk/src/main/java/org/dromara/risk/service/impl/RiskInspectionTaskServiceImpl.java

@@ -1,5 +1,6 @@
 package org.dromara.risk.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -13,6 +14,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.risk.domain.RiskInspectionTask;
 import org.dromara.risk.domain.bo.RiskInspectionTaskBo;
 import org.dromara.risk.domain.RiskInspectionRecord;
+import org.dromara.risk.domain.vo.MobileInspectionTaskVo;
 import org.dromara.risk.domain.vo.RiskInspectionRecordVo;
 import org.dromara.risk.domain.vo.InspectionStatsVo;
 import org.dromara.risk.domain.vo.RiskInspectionTaskVo;
@@ -22,10 +24,14 @@ import org.dromara.risk.service.IRiskInspectionTaskService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -181,5 +187,106 @@ public class RiskInspectionTaskServiceImpl implements IRiskInspectionTaskService
         return vo;
     }
 
+    // ===================== 移动端接口实现 =====================
+
+    /**
+     * 移动端-查询我的巡检任务列表
+     */
+    @Override
+    public TableDataInfo<MobileInspectionTaskVo> queryMobileList(Long inspectorId, String status, PageQuery pageQuery) {
+        // 1. 查任务(分页)
+        LambdaQueryWrapper<RiskInspectionTask> lqw = Wrappers.<RiskInspectionTask>lambdaQuery();
+        if (ObjectUtil.isNotNull(inspectorId)) {
+            lqw.eq(RiskInspectionTask::getInspectorId, inspectorId);
+        }
+        // 不查 已删除 的
+        lqw.eq(RiskInspectionTask::getDelFlag, "0");
+        // 状态过滤(移动端状态 PENDING/PUNCHED/OVERDUE/FINISHED)
+        if (StringUtils.isBlank(status)) {
+            // 默认查 全部,但隐藏 已完成
+            lqw.ne(RiskInspectionTask::getTaskStatus, "已完成");
+        }
+        lqw.orderByDesc(RiskInspectionTask::getPlanStartTime);
+        Page<RiskInspectionTask> page = baseMapper.selectPage(pageQuery.build(), lqw);
+        List<MobileInspectionTaskVo> rows = page.getRecords().stream()
+            .map(this::convertToMobileVo)
+            .toList();
+        return TableDataInfo.build(new Page<MobileInspectionTaskVo>()
+            .setRecords(rows)
+            .setTotal(page.getTotal())
+            .setSize(page.getSize())
+            .setCurrent(page.getCurrent()));
+    }
+
+    /**
+     * 移动端-查询任务详情
+     */
+    @Override
+    public MobileInspectionTaskVo queryMobileDetail(Long id) {
+        RiskInspectionTask task = baseMapper.selectById(id);
+        Assert.notNull(task, "巡检任务不存在: " + id);
+        return convertToMobileVo(task);
+    }
+
+    /**
+     * 移动端-校验二维码
+     */
+    @Override
+    public boolean verifyQrCode(Long taskId, String qrCode) {
+        if (ObjectUtil.isNull(taskId) || StringUtils.isBlank(qrCode)) {
+            return false;
+        }
+        RiskInspectionTask task = baseMapper.selectById(taskId);
+        if (task == null || StringUtils.isBlank(task.getQrCode())) {
+            return false;
+        }
+        return task.getQrCode().trim().equals(qrCode.trim());
+    }
+
+    /**
+     * 实体 → 移动端 VO
+     * <p>
+     * 计算字段:
+     * - punchType  : 1-扫码 0-常规(基于 scan_enabled/ check_method)
+     * - punchStatus: PENDING-待打卡 / PUNCHED-已打卡 / OVERDUE-已逾期 / FINISHED-已完成
+     * - punchResult: 0-正常 / 1-异常 / null-未打卡 (基于最新一条 record)
+     */
+    private MobileInspectionTaskVo convertToMobileVo(RiskInspectionTask task) {
+        MobileInspectionTaskVo vo = new MobileInspectionTaskVo();
+        BeanUtil.copyProperties(task, vo);
+
+        // 1. 打卡类型
+        boolean isScan = "1".equals(task.getScanEnabled())
+            || "扫码".equals(task.getCheckMethod());
+        vo.setPunchType(isScan ? "SCAN" : "NORMAL");
+
+        // 2. 查最近一条记录
+        RiskInspectionRecord latest = recordMapper.selectOne(
+            Wrappers.<RiskInspectionRecord>lambdaQuery()
+                .eq(RiskInspectionRecord::getTaskId, task.getId())
+                .orderByDesc(RiskInspectionRecord::getCheckInTime)
+                .last("LIMIT 1")
+        );
+
+        // 3. 任务原始状态
+        String originStatus = task.getTaskStatus();
+        Date now = new Date();
+
+        // 4. 状态计算
+        if ("已完成".equals(originStatus)) {
+            vo.setPunchStatus("FINISHED");
+        } else if (latest != null) {
+            // 已有打卡记录
+            vo.setPunchStatus("PUNCHED");
+            vo.setPunchTime(latest.getCheckInTime());
+            vo.setPunchResult(latest.getResult());
+        } else if (task.getPlanEndTime() != null && task.getPlanEndTime().before(now)) {
+            // 超时且未打卡
+            vo.setPunchStatus("OVERDUE");
+        } else {
+            vo.setPunchStatus("PENDING");
+        }
+        return vo;
+    }
 
 }

+ 20 - 0
sql/risk_control_platform.sql

@@ -1336,6 +1336,7 @@ CREATE TABLE `risk_inspection_record` (
   `inspector_id` bigint DEFAULT NULL COMMENT '巡检人ID',
   `inspector_name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '巡检人',
   `record_status` varchar(20) COLLATE utf8mb4_general_ci DEFAULT '进行中' COMMENT '记录状态',
+  `result` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '打卡结果(0正常 1异常)',
   `problem_count` int DEFAULT '0' COMMENT '问题数',
   `total_count` int DEFAULT '0' COMMENT '总数',
   `remark` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
@@ -1434,6 +1435,7 @@ CREATE TABLE `risk_inspection_task` (
   `cycle` varchar(20) COLLATE utf8mb4_general_ci DEFAULT '每日' COMMENT '巡检周期(每日/每周/每月)',
   `scan_enabled` char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '是否开启扫码打卡(0否 1是)',
   `check_method` varchar(20) COLLATE utf8mb4_general_ci DEFAULT '常规' COMMENT '打卡方式(常规/扫码)',
+  `qr_code` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '巡检点二维码内容(扫码打卡校验用)',
   `auditor_id` bigint DEFAULT NULL COMMENT '审核人ID',
   `auditor_name` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '审核人',
   `day_of_week` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '执行星期(每周模式,逗号分隔)',
@@ -3875,3 +3877,21 @@ UNLOCK TABLES;
 /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
 
 -- Dump completed on 2026-06-29 17:29:16
+
+-- ============================================================
+-- 风控品控移动端 巡检打卡 字段升级补丁 (v1.0.1)
+-- 适用:已部署 1.0.0 的库增量升级
+-- 日期:2026-06-21
+-- ============================================================
+
+-- 1. 巡检任务表:新增二维码内容字段(扫码打卡校验)
+ALTER TABLE `risk_inspection_task`
+  ADD COLUMN `qr_code` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL
+  COMMENT '巡检点二维码内容(扫码打卡校验用)'
+  AFTER `check_method`;
+
+-- 2. 巡检记录表:新增打卡结果字段(正常/异常)
+ALTER TABLE `risk_inspection_record`
+  ADD COLUMN `result` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL
+  COMMENT '打卡结果(0正常 1异常)'
+  AFTER `record_status`;