diff --git a/.ralph/fix_plan.md b/.ralph/fix_plan.md
index 83457ef..1312f77 100644
--- a/.ralph/fix_plan.md
+++ b/.ralph/fix_plan.md
@@ -26,12 +26,14 @@
- 逻辑: area_id → 查 ops_bus_area 获取 function_type → 查 ops_inspection_template 加载检查项
- 返回: 检查项列表
-### 4. 蓝牙位置校验逻辑
-- [ ] InspectionLocationService(位置校验服务)
+### 4. 蓝牙位置校验逻辑 ✅
+- [x] InspectionLocationService(位置校验服务)
- 入参: area_id + detected_beacons
- 查询 ops_area_device_relation 获取该区域绑定的信标列表
- 匹配算法: 至少1个绑定信标匹配,且 RSSI > 阈值
- 返回: 校验通过/失败
+- [x] InspectionController + verify-location 端点
+- [x] DetectedBeaconVO, LocationVerifyResultVO
## Medium Priority
@@ -72,6 +74,7 @@
- [x] Task 1: 数据库表 + DO + Mapper(3表、3DO、3Mapper)
- [x] Task 2: 巡检模板 CRUD(Service + Impl + Controller + VOs)
- [x] Task 3: 获取动态表单接口(list-by-area,area_id → function_type → template 查询链路)
+- [x] Task 4: 蓝牙位置校验(InspectionLocationService + verify-location 端点)
## Notes
- 巡检是保洁业务线内的子功能,代码放 viewsh-module-environment-biz
diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/DetectedBeaconVO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/DetectedBeaconVO.java
new file mode 100644
index 0000000..d828f92
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/DetectedBeaconVO.java
@@ -0,0 +1,20 @@
+package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "蓝牙信标检测数据")
+@Data
+public class DetectedBeaconVO {
+
+ @Schema(description = "信标MAC地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "F0:C8:60:1D:10:BB")
+ @NotBlank(message = "信标MAC地址不能为空")
+ private String mac;
+
+ @Schema(description = "信号强度(RSSI)", requiredMode = Schema.RequiredMode.REQUIRED, example = "-65")
+ @NotNull(message = "信号强度不能为空")
+ private Integer rssi;
+
+}
diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/LocationVerifyResultVO.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/LocationVerifyResultVO.java
new file mode 100644
index 0000000..4a466f9
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/controller/admin/inspection/vo/LocationVerifyResultVO.java
@@ -0,0 +1,28 @@
+package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Schema(description = "蓝牙位置校验结果")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LocationVerifyResultVO {
+
+ @Schema(description = "校验是否通过")
+ private Boolean passed;
+
+ @Schema(description = "校验消息")
+ private String message;
+
+ public static LocationVerifyResultVO success() {
+ return new LocationVerifyResultVO(true, "位置校验通过");
+ }
+
+ public static LocationVerifyResultVO fail(String message) {
+ return new LocationVerifyResultVO(false, message);
+ }
+
+}
diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationService.java
new file mode 100644
index 0000000..f2edb10
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationService.java
@@ -0,0 +1,25 @@
+package com.viewsh.module.ops.environment.service.inspection;
+
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.DetectedBeaconVO;
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.LocationVerifyResultVO;
+
+import java.util.List;
+
+/**
+ * 巡检蓝牙位置校验 Service
+ */
+public interface InspectionLocationService {
+
+ /**
+ * 校验巡检人员是否在指定区域
+ *
+ * 逻辑:查询该区域绑定的 BEACON 类型设备,
+ * 匹配检测到的蓝牙信标列表,至少1个绑定信标匹配且 RSSI 大于阈值即通过
+ *
+ * @param areaId 区域ID
+ * @param detectedBeacons 检测到的蓝牙信标列表
+ * @return 校验结果
+ */
+ LocationVerifyResultVO verifyLocation(Long areaId, List detectedBeacons);
+
+}
diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationServiceImpl.java
new file mode 100644
index 0000000..c46bbd9
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/inspection/InspectionLocationServiceImpl.java
@@ -0,0 +1,100 @@
+package com.viewsh.module.ops.environment.service.inspection;
+
+import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
+import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.DetectedBeaconVO;
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.LocationVerifyResultVO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 巡检蓝牙位置校验 Service 实现
+ */
+@Service
+@Validated
+@Slf4j
+public class InspectionLocationServiceImpl implements InspectionLocationService {
+
+ private static final String RELATION_TYPE_BEACON = "BEACON";
+ private static final int DEFAULT_RSSI_THRESHOLD = -70;
+
+ @Resource
+ private OpsAreaDeviceRelationMapper areaDeviceRelationMapper;
+
+ @Override
+ public LocationVerifyResultVO verifyLocation(Long areaId, List detectedBeacons) {
+ if (detectedBeacons == null || detectedBeacons.isEmpty()) {
+ return LocationVerifyResultVO.fail("未检测到蓝牙信标");
+ }
+
+ // 查询该区域绑定的 BEACON 设备
+ List beaconRelations =
+ areaDeviceRelationMapper.selectListByAreaIdAndRelationType(areaId, RELATION_TYPE_BEACON);
+ if (beaconRelations.isEmpty()) {
+ // 区域未绑定信标,视为不需要位置校验,直接通过
+ log.info("[verifyLocation] 区域 {} 未绑定蓝牙信标,跳过位置校验", areaId);
+ return LocationVerifyResultVO.success();
+ }
+
+ // 匹配逻辑:至少1个绑定信标匹配且 RSSI > 阈值
+ for (OpsAreaDeviceRelationDO relation : beaconRelations) {
+ String boundBeaconMac = extractBeaconMac(relation);
+ if (boundBeaconMac == null) {
+ continue;
+ }
+ int rssiThreshold = extractRssiThreshold(relation);
+
+ for (DetectedBeaconVO detected : detectedBeacons) {
+ if (boundBeaconMac.equalsIgnoreCase(detected.getMac())
+ && detected.getRssi() >= rssiThreshold) {
+ log.info("[verifyLocation] 区域 {} 位置校验通过,匹配信标 {},RSSI={}",
+ areaId, detected.getMac(), detected.getRssi());
+ return LocationVerifyResultVO.success();
+ }
+ }
+ }
+
+ return LocationVerifyResultVO.fail("未匹配到该区域的蓝牙信标,请确认是否在正确位置");
+ }
+
+ /**
+ * 从设备关联的 configData 中提取信标 MAC 地址
+ */
+ private String extractBeaconMac(OpsAreaDeviceRelationDO relation) {
+ Map configData = relation.getConfigData();
+ if (configData == null) {
+ // 退回到 deviceKey 作为 MAC 标识
+ return relation.getDeviceKey();
+ }
+ Object beaconMac = configData.get("beaconMac");
+ if (beaconMac != null) {
+ return beaconMac.toString();
+ }
+ return relation.getDeviceKey();
+ }
+
+ /**
+ * 从设备关联的 configData 中提取 RSSI 阈值
+ */
+ @SuppressWarnings("unchecked")
+ private int extractRssiThreshold(OpsAreaDeviceRelationDO relation) {
+ Map configData = relation.getConfigData();
+ if (configData == null) {
+ return DEFAULT_RSSI_THRESHOLD;
+ }
+ Object enter = configData.get("enter");
+ if (enter instanceof Map) {
+ Object threshold = ((Map) enter).get("rssiThreshold");
+ if (threshold instanceof Number) {
+ return ((Number) threshold).intValue();
+ }
+ }
+ return DEFAULT_RSSI_THRESHOLD;
+ }
+
+}
diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/inspection/InspectionController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/inspection/InspectionController.java
new file mode 100644
index 0000000..7e8b693
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/inspection/InspectionController.java
@@ -0,0 +1,42 @@
+package com.viewsh.module.ops.controller.admin.inspection;
+
+import com.viewsh.framework.common.pojo.CommonResult;
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.DetectedBeaconVO;
+import com.viewsh.module.ops.environment.controller.admin.inspection.vo.LocationVerifyResultVO;
+import com.viewsh.module.ops.environment.service.inspection.InspectionLocationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+import static com.viewsh.framework.common.pojo.CommonResult.success;
+
+/**
+ * 管理后台 - 巡检 Controller
+ */
+@Tag(name = "管理后台 - 巡检")
+@RestController
+@RequestMapping("/ops/inspection")
+@Validated
+public class InspectionController {
+
+ @Resource
+ private InspectionLocationService inspectionLocationService;
+
+ @PostMapping("/verify-location")
+ @Operation(summary = "蓝牙位置校验")
+ @Parameter(name = "areaId", description = "区域ID", required = true)
+ @PreAuthorize("@ss.hasPermission('ops:inspection:create')")
+ public CommonResult verifyLocation(
+ @RequestParam("areaId") Long areaId,
+ @Valid @RequestBody List detectedBeacons) {
+ return success(inspectionLocationService.verifyLocation(areaId, detectedBeacons));
+ }
+
+}