11 Commits

Author SHA1 Message Date
lzh
5ce1561437 Merge branch 'master' of http://124.222.218.198:3000/XW-AIOT/aiot-platform-cloud into feature/cleaning-inspection 2026-03-18 23:03:44 +08:00
lzh
01e9a556ab Merge branch 'master' of http://124.222.218.198:3000/XW-AIOT/aiot-platform-cloud into feature/cleaning-inspection 2026-03-18 23:01:09 +08:00
lzh
e3882e1c2f fix(ops): code review 修复巡检模块6项问题
1. @Async 指定 ops-task-executor 线程池,避免使用默认线程池
2. 归属判定无工单/无标准时长时标记为 ATTRIBUTION_NORMAL(3),不再静默跳过
3. 补充 completionSeconds 字段语义注释和 standardDuration 单位转换说明
4. 整改工单默认时长 30 提取为 DEFAULT_RECTIFICATION_DURATION_MINUTES 常量
5. SQL 补充 idx_generated_order_id 和 idx_template_id 索引

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:40:36 +08:00
lzh
f70402587d feat(ops): 巡检统计接口(合格率、不合格热点区域 TOP10)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:13:13 +08:00
lzh
743875e65e feat(ops): 巡检记录分页查询接口(按区域/巡检员/结果/时间筛选)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:11:21 +08:00
lzh
23cf3b62b2 fix(ops): 修复巡检异步处理的 @Async 自调用和事务可见性问题
Code review 发现两个关键缺陷:
1. @Async 自调用:triggerAttributionAsync() 在同一类内调用,
   Spring AOP 代理不生效,实际同步执行
2. 事务可见性:异步任务可能在事务提交前读取未持久化数据

修复方案:
- 提取 InspectionAsyncHandler(独立 @Component),@Async 通过代理生效
- 使用 TransactionSynchronizationManager.afterCommit() 确保事务提交后触发
- 修复 InspectionRectificationServiceImpl 的 null 安全问题
- 修复 InspectionTemplateServiceImpl 更新路径缺少 id 空校验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:30:01 +08:00
lzh
162bf1d20d feat(ops): 巡检整改工单自动创建(Task 7)
- InspectionRectificationService + Impl: 不合格巡检自动创建整改工单
  - 复用 CleanOrderService.createAutoCleanOrder() 对接现有工单引擎
  - sourceType = INSPECTION, priority = P1, cleaningType = SPOT
  - generated_order_id 回写到巡检记录
- InspectionRecordServiceImpl: 异步流程增加整改工单创建步骤
  - 归属判定与整改工单创建独立 try/catch,互不阻塞

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:23:34 +08:00
lzh
3120b1911d feat(ops): 巡检归属判定异步服务(Task 6)
- InspectionAttributionService + Impl: 归属判定核心逻辑
  - 回溯区域最近 COMPLETED 工单,获取 completionSeconds
  - 与 ops_bus_area.standardDuration 比较判定责任归属
  - T_stay >= threshold → 突发状况(2),< threshold → 个人责任(1)
  - 判定结果回写 inspection_record(lastOrderId, stayDuration, attributionResult)
- InspectionRecordServiceImpl: 注入 AttributionService,异步调用含异常兜底

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:18:56 +08:00
lzh
e4dde8dbc1 feat(ops): 巡检结果提交接口(Task 5)
- InspectionRecordService + Impl: 提交巡检主记录+明细,同事务保存
- 自动判定 resultStatus:任一项不合格则整体不合格
- 不合格时异步触发归属判定(Task 6 占位)
- InspectionSubmitReqVO/ItemVO: 带校验注解的请求 VO
- InspectionRecordRespVO: 巡检记录响应 VO
- InspectionController 新增 POST /submit 端点
- ErrorCodeConstants 新增 INSPECTION_RECORD_NOT_FOUND

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:15:01 +08:00
lzh
fa45d94247 feat(ops): 蓝牙位置校验服务及接口(Task 4) 2026-03-05 17:07:48 +08:00
lzh
99651be386 feat(ops): 巡检模板CRUD及动态表单接口(Task 2+3) 2026-03-05 17:04:45 +08:00
33 changed files with 1608 additions and 0 deletions

84
.ralph/fix_plan.md Normal file
View File

@@ -0,0 +1,84 @@
# Ralph Fix Plan - 保洁巡检模块
## 需求文档
参考: docs/物业服务AIoT平台-保洁服务模块.md模块二巡检小程序后端
## High Priority
### 1. 数据库表 + DO + Mapper ✅
- [x] 创建 `ops_inspection_template` 表及 SQL巡检检查项模板
- [x] 创建 `ops_inspection_record` 表及 SQL巡检主记录
- [x] 创建 `ops_inspection_record_item` 表及 SQL巡检明细
- [x] 创建对应 DO 类: OpsInspectionTemplateDO, OpsInspectionRecordDO, OpsInspectionRecordItemDO
- [x] 创建对应 Mapper: OpsInspectionTemplateMapper, OpsInspectionRecordMapper, OpsInspectionRecordItemMapper
- SQL: sql/mysql/aiot_ops_inspection.sql
- DO: environment-biz/dal/dataobject/inspection/
- Mapper: environment-biz/dal/mysql/inspection/
### 2. 巡检模板 CRUD管理后台配置检查项
- [x] InspectionTemplateService 接口 + Impl增删改查按 function_type 查询)
- [x] InspectionTemplateController管理端 REST API
- [x] 请求/响应 VO 类
### 3. 获取动态表单接口 ✅
- [x] GET /admin-api/ops/inspection/template/list-by-area
- 入参: area_id + detected_beacons蓝牙指纹数组
- 逻辑: area_id → 查 ops_bus_area 获取 function_type → 查 ops_inspection_template 加载检查项
- 返回: 检查项列表
### 4. 蓝牙位置校验逻辑 ✅
- [x] InspectionLocationService位置校验服务
- 入参: area_id + detected_beacons
- 查询 ops_area_device_relation 获取该区域绑定的信标列表
- 匹配算法: 至少1个绑定信标匹配且 RSSI > 阈值
- 返回: 校验通过/失败
- [x] InspectionController + verify-location 端点
- [x] DetectedBeaconVO, LocationVerifyResultVO
## Medium Priority
### 5. 提交巡检结果接口
- [ ] POST /admin-api/ops/inspection/submit
- 入参: area_id, is_location_exception, items[{template_id, is_passed, remark}]
- 逻辑:
1. 保存 ops_inspection_record 主记录
2. 批量保存 ops_inspection_record_item 明细
3. 判定 result_status: 任意一项 is_passed=false → 不合格
4. 不合格时触发异步归属判定
- [ ] InspectionRecordService 接口 + Impl
- [ ] InspectionController巡检提交 REST API
### 6. 归属判定异步逻辑
- [ ] InspectionAttributionService归属判定服务
- 以巡检提交时间为基点,回溯该区域上一个 COMPLETED 工单
- 获取保洁员实际停留时长 T_stay来自 ops_order.actual_duration 或工单扩展)
- 获取区域标准时长 clean_threshold来自 ops_bus_area.standard_duration
- 判定:
- T_stay >= clean_threshold → attribution_result=2突发状况不扣分
- T_stay < clean_threshold attribution_result=1个人责任扣信用分
- 更新 ops_inspection_record attribution_result, last_order_id, stay_duration
### 7. 整改工单自动创建
- [ ] 不合格巡检触发创建整改派单
- 调用现有 OpsOrderService / OrderLifecycleManager 创建工单
- source_type = INSPECTION
- 关联 generated_order_id 回写到 ops_inspection_record
- 对接工单引擎的优先级和派单逻辑
## Low Priority
- [ ] 巡检记录查询接口分页按区域/时间筛选
- [ ] 巡检统计接口合格率不合格热点区域
## Completed
- [x] 需求分析与表结构设计
- [x] Task 1: 数据库表 + DO + Mapper3表3DO3Mapper
- [x] Task 2: 巡检模板 CRUDService + Impl + Controller + VOs
- [x] Task 3: 获取动态表单接口list-by-areaarea_id function_type template 查询链路
- [x] Task 4: 蓝牙位置校验InspectionLocationService + verify-location 端点
## Notes
- 巡检是保洁业务线内的子功能代码放 viewsh-module-environment-biz
- 复用 ops_area_device_relation 做蓝牙信标校验不新建 iot_device_location
- 复用 ops_bus_area.standard_duration 作为 clean_threshold不新建 ops_rule_config
- Controller 放在 ops-server 模块的 controller/admin/inspection/
- 遵循现有 CleanOrderService 的编码模式

View File

@@ -0,0 +1,71 @@
-- =============================================
-- 巡检模块表结构
-- =============================================
-- ----------------------------
-- 巡检检查项模板表
-- ----------------------------
DROP TABLE IF EXISTS `ops_inspection_template`;
CREATE TABLE `ops_inspection_template` (
`id` bigint NOT NULL COMMENT '模板ID',
`function_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '功能类型(关联 ops_bus_area.function_type',
`item_title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '检查项标题',
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序序号',
`is_active` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否启用',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
KEY `idx_function_type` (`function_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检检查项模板';
-- ----------------------------
-- 巡检主记录表
-- ----------------------------
DROP TABLE IF EXISTS `ops_inspection_record`;
CREATE TABLE `ops_inspection_record` (
`id` bigint NOT NULL COMMENT '巡检记录ID',
`area_id` bigint NOT NULL COMMENT '区域ID',
`inspector_id` bigint NOT NULL COMMENT '巡检员用户ID',
`is_location_exception` tinyint NOT NULL DEFAULT 0 COMMENT '位置是否异常0正常 1异常',
`result_status` tinyint COMMENT '巡检结果0不合格 1合格',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '备注',
`last_order_id` bigint COMMENT '归属判定-上一个完成工单ID',
`stay_duration` int COMMENT '归属判定-保洁员停留时长(秒)',
`attribution_result` tinyint COMMENT '归属判定结果1个人责任 2突发状况 3正常',
`generated_order_id` bigint COMMENT '整改工单ID',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
KEY `idx_area_id` (`area_id`),
KEY `idx_inspector_id` (`inspector_id`),
KEY `idx_generated_order_id` (`generated_order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检主记录';
-- ----------------------------
-- 巡检明细表
-- ----------------------------
DROP TABLE IF EXISTS `ops_inspection_record_item`;
CREATE TABLE `ops_inspection_record_item` (
`id` bigint NOT NULL COMMENT '明细ID',
`record_id` bigint NOT NULL COMMENT '巡检记录ID',
`template_id` bigint NOT NULL COMMENT '模板检查项ID',
`is_passed` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否合格',
`remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '备注',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`),
KEY `idx_record_id` (`record_id`),
KEY `idx_template_id` (`template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检明细';

View File

@@ -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;
}

View File

@@ -0,0 +1,31 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import com.viewsh.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 巡检记录分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class InspectionRecordPageReqVO extends PageParam {
@Schema(description = "区域ID", example = "1")
private Long areaId;
@Schema(description = "巡检员用户ID", example = "100")
private Long inspectorId;
@Schema(description = "巡检结果0不合格 1合格", example = "1")
private Integer resultStatus;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,39 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 巡检记录 Response VO")
@Data
public class InspectionRecordRespVO {
@Schema(description = "巡检记录ID", example = "1024")
private Long id;
@Schema(description = "区域ID", example = "1")
private Long areaId;
@Schema(description = "巡检员用户ID", example = "100")
private Long inspectorId;
@Schema(description = "位置是否异常0正常 1异常", example = "0")
private Integer isLocationException;
@Schema(description = "巡检结果0不合格 1合格", example = "1")
private Integer resultStatus;
@Schema(description = "备注", example = "检查完成")
private String remark;
@Schema(description = "归属判定结果1个人责任 2突发状况 3正常", example = "3")
private Integer attributionResult;
@Schema(description = "整改工单ID", example = "2048")
private Long generatedOrderId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,22 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 巡检统计查询 Request VO")
@Data
public class InspectionStatsReqVO {
@Schema(description = "区域ID可选不传则统计全部区域", example = "1")
private Long areaId;
@Schema(description = "时间范围")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,48 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Schema(description = "管理后台 - 巡检统计 Response VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InspectionStatsRespVO {
@Schema(description = "总巡检次数", example = "100")
private Long totalCount;
@Schema(description = "合格次数", example = "85")
private Long passedCount;
@Schema(description = "不合格次数", example = "15")
private Long failedCount;
@Schema(description = "合格率(百分比)", example = "85.00")
private Double passRate;
@Schema(description = "不合格热点区域(按不合格次数降序)")
private List<AreaFailStat> hotSpotAreas;
@Schema(description = "区域不合格统计")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AreaFailStat {
@Schema(description = "区域ID", example = "1")
private Long areaId;
@Schema(description = "不合格次数", example = "5")
private Long failedCount;
}
}

View File

@@ -0,0 +1,22 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - 巡检提交明细项 VO")
@Data
public class InspectionSubmitItemVO {
@Schema(description = "模板检查项ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模板检查项ID不能为空")
private Long templateId;
@Schema(description = "是否合格", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否合格不能为空")
private Boolean isPassed;
@Schema(description = "备注", example = "地面有明显污渍")
private String remark;
}

View File

@@ -0,0 +1,31 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 提交巡检结果 Request VO")
@Data
public class InspectionSubmitReqVO {
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "区域ID不能为空")
private Long areaId;
@Schema(description = "位置是否异常0正常 1异常", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "位置异常标志不能为空")
private Integer isLocationException;
@Schema(description = "备注", example = "卫生间地面有污渍")
private String remark;
@Schema(description = "巡检明细项列表", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "巡检明细项不能为空")
@Valid
private List<InspectionSubmitItemVO> items;
}

View File

@@ -0,0 +1,22 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import com.viewsh.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - 巡检模板分页查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class InspectionTemplatePageReqVO extends PageParam {
@Schema(description = "功能类型", example = "TOILET")
private String functionType;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "检查项标题(模糊搜索)", example = "地面")
private String itemTitle;
}

View File

@@ -0,0 +1,30 @@
package com.viewsh.module.ops.environment.controller.admin.inspection.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 巡检模板 Response VO")
@Data
public class InspectionTemplateRespVO {
@Schema(description = "模板ID", example = "1")
private Long id;
@Schema(description = "功能类型", example = "TOILET")
private String functionType;
@Schema(description = "检查项标题", example = "地面是否干净")
private String itemTitle;
@Schema(description = "排序序号", example = "1")
private Integer sortOrder;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,30 @@
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 = "管理后台 - 巡检模板创建/修改 Request VO")
@Data
public class InspectionTemplateSaveReqVO {
@Schema(description = "模板ID修改时必填", example = "1")
private Long id;
@Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "TOILET")
@NotBlank(message = "功能类型不能为空")
private String functionType;
@Schema(description = "检查项标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "地面是否干净")
@NotBlank(message = "检查项标题不能为空")
private String itemTitle;
@Schema(description = "排序序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "排序序号不能为空")
private Integer sortOrder;
@Schema(description = "是否启用", example = "true")
private Boolean isActive;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,64 @@
package com.viewsh.module.ops.environment.dal.dataobject.inspection;
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 巡检主记录 DO
*/
@TableName("ops_inspection_record")
@KeySequence("ops_inspection_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsInspectionRecordDO extends BaseDO {
/**
* 巡检记录ID
*/
@TableId
private Long id;
/**
* 区域ID
*/
private Long areaId;
/**
* 巡检员用户ID
*/
private Long inspectorId;
/**
* 位置是否异常0正常 1异常
*/
private Integer isLocationException;
/**
* 巡检结果0不合格 1合格
*/
private Integer resultStatus;
/**
* 备注
*/
private String remark;
/**
* 归属判定-上一个完成工单ID
*/
private Long lastOrderId;
/**
* 归属判定-保洁员停留时长(秒)
*/
private Integer stayDuration;
/**
* 归属判定结果1个人责任 2突发状况 3正常
*/
private Integer attributionResult;
/**
* 整改工单ID
*/
private Long generatedOrderId;
}

View File

@@ -0,0 +1,44 @@
package com.viewsh.module.ops.environment.dal.dataobject.inspection;
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 巡检明细 DO
*/
@TableName("ops_inspection_record_item")
@KeySequence("ops_inspection_record_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsInspectionRecordItemDO extends BaseDO {
/**
* 明细ID
*/
@TableId
private Long id;
/**
* 巡检记录ID
*/
private Long recordId;
/**
* 模板检查项ID
*/
private Long templateId;
/**
* 是否合格
*/
private Boolean isPassed;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,44 @@
package com.viewsh.module.ops.environment.dal.dataobject.inspection;
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 巡检检查项模板 DO
*/
@TableName("ops_inspection_template")
@KeySequence("ops_inspection_template_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsInspectionTemplateDO extends BaseDO {
/**
* 模板ID
*/
@TableId
private Long id;
/**
* 功能类型(关联 ops_bus_area.function_type
*/
private String functionType;
/**
* 检查项标题
*/
private String itemTitle;
/**
* 排序序号
*/
private Integer sortOrder;
/**
* 是否启用
*/
private Boolean isActive;
}

View File

@@ -0,0 +1,22 @@
package com.viewsh.module.ops.environment.dal.mysql.inspection;
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 巡检明细 Mapper
*/
@Mapper
public interface OpsInspectionRecordItemMapper extends BaseMapperX<OpsInspectionRecordItemDO> {
/**
* 按巡检记录ID查询明细
*/
default List<OpsInspectionRecordItemDO> selectListByRecordId(Long recordId) {
return selectList(OpsInspectionRecordItemDO::getRecordId, recordId);
}
}

View File

@@ -0,0 +1,13 @@
package com.viewsh.module.ops.environment.dal.mysql.inspection;
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 巡检主记录 Mapper
*/
@Mapper
public interface OpsInspectionRecordMapper extends BaseMapperX<OpsInspectionRecordDO> {
}

View File

@@ -0,0 +1,26 @@
package com.viewsh.module.ops.environment.dal.mysql.inspection;
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionTemplateDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 巡检检查项模板 Mapper
*/
@Mapper
public interface OpsInspectionTemplateMapper extends BaseMapperX<OpsInspectionTemplateDO> {
/**
* 按功能类型查询启用的检查项,按排序序号升序
*/
default List<OpsInspectionTemplateDO> selectListByFunctionType(String functionType) {
return selectList(new LambdaQueryWrapperX<OpsInspectionTemplateDO>()
.eq(OpsInspectionTemplateDO::getFunctionType, functionType)
.eq(OpsInspectionTemplateDO::getIsActive, true)
.orderByAsc(OpsInspectionTemplateDO::getSortOrder));
}
}

View File

@@ -0,0 +1,45 @@
package com.viewsh.module.ops.environment.service.inspection;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 巡检异步处理器(独立 Bean解决 @Async 自调用问题)
*
* 负责在事务提交后异步执行:
* 1. 归属判定
* 2. 整改工单创建
*/
@Component
@Slf4j
public class InspectionAsyncHandler {
@Resource
private InspectionAttributionService inspectionAttributionService;
@Resource
private InspectionRectificationService inspectionRectificationService;
/**
* 异步处理不合格巡检的后续逻辑
*/
@Async("ops-task-executor")
public void handleFailedInspection(Long recordId, Long areaId) {
// 1. 归属判定
try {
inspectionAttributionService.determineAttribution(recordId, areaId);
} catch (Exception e) {
log.error("[handleFailedInspection] 归属判定异常: recordId={}, areaId={}", recordId, areaId, e);
}
// 2. 创建整改工单(不论归属判定结果,不合格即需整改)
try {
inspectionRectificationService.createRectificationOrder(recordId, areaId);
} catch (Exception e) {
log.error("[handleFailedInspection] 整改工单创建异常: recordId={}, areaId={}", recordId, areaId, e);
}
}
}

View File

@@ -0,0 +1,21 @@
package com.viewsh.module.ops.environment.service.inspection;
/**
* 巡检归属判定 Service 接口
*
* 当巡检结果为不合格时,异步执行归属判定:
* 1. 回溯该区域最近一个 COMPLETED 工单
* 2. 比较保洁员实际停留时长与区域标准时长
* 3. 判定责任归属并回写巡检记录
*/
public interface InspectionAttributionService {
/**
* 执行归属判定
*
* @param recordId 巡检记录ID
* @param areaId 区域ID
*/
void determineAttribution(Long recordId, Long areaId);
}

View File

@@ -0,0 +1,99 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
import com.viewsh.module.ops.environment.dal.mysql.inspection.OpsInspectionRecordMapper;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
/**
* 巡检归属判定 Service 实现
*/
@Service
@Validated
@Slf4j
public class InspectionAttributionServiceImpl implements InspectionAttributionService {
/** 归属判定:个人责任(停留时长不足) */
private static final int ATTRIBUTION_PERSONAL = 1;
/** 归属判定:突发状况(停留时长达标) */
private static final int ATTRIBUTION_EMERGENCY = 2;
/** 归属判定:正常(合格巡检,无需判定) */
private static final int ATTRIBUTION_NORMAL = 3;
@Resource
private OpsInspectionRecordMapper inspectionRecordMapper;
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void determineAttribution(Long recordId, Long areaId) {
// 1. 回溯该区域最近一个 COMPLETED 工单
OpsOrderDO lastOrder = opsOrderMapper.selectOne(new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getAreaId, areaId)
.eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.COMPLETED.getStatus())
.orderByDesc(OpsOrderDO::getEndTime)
.last("LIMIT 1"));
if (lastOrder == null) {
log.warn("[determineAttribution] 区域 {} 无已完成工单,标记为正常: recordId={}", areaId, recordId);
updateAttributionResult(recordId, null, null, ATTRIBUTION_NORMAL);
return;
}
// 2. 获取保洁员实际停留时长(秒)
// completionSeconds 记录的是工单从开始到完成的耗时,即实际作业时长
Integer stayDurationSeconds = lastOrder.getCompletionSeconds();
if (stayDurationSeconds == null) {
stayDurationSeconds = 0;
}
// 3. 获取区域标准时长standardDuration 单位为分钟,转换为秒)
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
if (area == null || area.getStandardDuration() == null) {
log.warn("[determineAttribution] 区域 {} 无标准时长配置,标记为正常: recordId={}", areaId, recordId);
updateAttributionResult(recordId, lastOrder.getId(), stayDurationSeconds, ATTRIBUTION_NORMAL);
return;
}
int standardDurationSeconds = area.getStandardDuration() * 60; // 分钟 → 秒
// 4. 判定归属
// T_stay >= clean_threshold → 突发状况(保洁员已做到位,不扣分)
// T_stay < clean_threshold → 个人责任(保洁时长不足,扣信用分)
int attributionResult = stayDurationSeconds >= standardDurationSeconds
? ATTRIBUTION_EMERGENCY
: ATTRIBUTION_PERSONAL;
log.info("[determineAttribution] 归属判定完成: recordId={}, areaId={}, lastOrderId={}, " +
"stayDuration={}s, standardDuration={}s({}min), result={}",
recordId, areaId, lastOrder.getId(),
stayDurationSeconds, standardDurationSeconds, area.getStandardDuration(), attributionResult);
// 5. 回写巡检记录
updateAttributionResult(recordId, lastOrder.getId(), stayDurationSeconds, attributionResult);
}
private void updateAttributionResult(Long recordId, Long lastOrderId,
Integer stayDuration, int attributionResult) {
OpsInspectionRecordDO update = new OpsInspectionRecordDO();
update.setId(recordId);
update.setLastOrderId(lastOrderId);
update.setStayDuration(stayDuration);
update.setAttributionResult(attributionResult);
inspectionRecordMapper.updateById(update);
}
}

View File

@@ -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 {
/**
* 校验巡检人员是否在指定区域
* <p>
* 逻辑:查询该区域绑定的 BEACON 类型设备,
* 匹配检测到的蓝牙信标列表至少1个绑定信标匹配且 RSSI 大于阈值即通过
*
* @param areaId 区域ID
* @param detectedBeacons 检测到的蓝牙信标列表
* @return 校验结果
*/
LocationVerifyResultVO verifyLocation(Long areaId, List<DetectedBeaconVO> detectedBeacons);
}

View File

@@ -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<DetectedBeaconVO> detectedBeacons) {
if (detectedBeacons == null || detectedBeacons.isEmpty()) {
return LocationVerifyResultVO.fail("未检测到蓝牙信标");
}
// 查询该区域绑定的 BEACON 设备
List<OpsAreaDeviceRelationDO> 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<String, Object> 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<String, Object> configData = relation.getConfigData();
if (configData == null) {
return DEFAULT_RSSI_THRESHOLD;
}
Object enter = configData.get("enter");
if (enter instanceof Map) {
Object threshold = ((Map<String, Object>) enter).get("rssiThreshold");
if (threshold instanceof Number) {
return ((Number) threshold).intValue();
}
}
return DEFAULT_RSSI_THRESHOLD;
}
}

View File

@@ -0,0 +1,40 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionRecordPageReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionStatsReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionStatsRespVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionSubmitReqVO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
/**
* 巡检记录 Service 接口
*/
public interface InspectionRecordService {
/**
* 提交巡检结果
*
* @param submitReqVO 提交请求
* @param inspectorId 巡检员用户ID
* @return 巡检记录ID
*/
Long submitInspection(InspectionSubmitReqVO submitReqVO, Long inspectorId);
/**
* 获得巡检记录分页
*
* @param pageReqVO 分页查询
* @return 巡检记录分页
*/
PageResult<OpsInspectionRecordDO> getRecordPage(InspectionRecordPageReqVO pageReqVO);
/**
* 获得巡检统计(合格率、不合格热点区域)
*
* @param reqVO 统计查询条件
* @return 巡检统计
*/
InspectionStatsRespVO getInspectionStats(InspectionStatsReqVO reqVO);
}

View File

@@ -0,0 +1,156 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionRecordPageReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionStatsReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionStatsRespVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionSubmitItemVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionSubmitReqVO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordItemDO;
import com.viewsh.module.ops.environment.dal.mysql.inspection.OpsInspectionRecordItemMapper;
import com.viewsh.module.ops.environment.dal.mysql.inspection.OpsInspectionRecordMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
/**
* 巡检记录 Service 实现
*/
@Service
@Validated
@Slf4j
public class InspectionRecordServiceImpl implements InspectionRecordService {
/** 巡检结果:合格 */
private static final int RESULT_STATUS_PASSED = 1;
/** 巡检结果:不合格 */
private static final int RESULT_STATUS_FAILED = 0;
@Resource
private OpsInspectionRecordMapper inspectionRecordMapper;
@Resource
private OpsInspectionRecordItemMapper inspectionRecordItemMapper;
@Resource
private InspectionAsyncHandler inspectionAsyncHandler;
@Override
@Transactional(rollbackFor = Exception.class)
public Long submitInspection(InspectionSubmitReqVO submitReqVO, Long inspectorId) {
// 1. 判定巡检结果:任一项不合格 → 整体不合格
boolean allPassed = submitReqVO.getItems().stream()
.allMatch(InspectionSubmitItemVO::getIsPassed);
int resultStatus = allPassed ? RESULT_STATUS_PASSED : RESULT_STATUS_FAILED;
// 2. 保存巡检主记录
OpsInspectionRecordDO record = OpsInspectionRecordDO.builder()
.areaId(submitReqVO.getAreaId())
.inspectorId(inspectorId)
.isLocationException(submitReqVO.getIsLocationException())
.resultStatus(resultStatus)
.remark(submitReqVO.getRemark())
.build();
inspectionRecordMapper.insert(record);
// 3. 批量保存巡检明细
List<OpsInspectionRecordItemDO> items = submitReqVO.getItems().stream()
.map(itemVO -> OpsInspectionRecordItemDO.builder()
.recordId(record.getId())
.templateId(itemVO.getTemplateId())
.isPassed(itemVO.getIsPassed())
.remark(itemVO.getRemark())
.build())
.toList();
inspectionRecordItemMapper.insertBatch(items);
// 4. 不合格时,在事务提交后异步触发归属判定 + 整改工单
if (resultStatus == RESULT_STATUS_FAILED) {
Long recordId = record.getId();
Long areaId = submitReqVO.getAreaId();
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
inspectionAsyncHandler.handleFailedInspection(recordId, areaId);
}
});
}
return record.getId();
}
@Override
public PageResult<OpsInspectionRecordDO> getRecordPage(InspectionRecordPageReqVO pageReqVO) {
return inspectionRecordMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<OpsInspectionRecordDO>()
.eqIfPresent(OpsInspectionRecordDO::getAreaId, pageReqVO.getAreaId())
.eqIfPresent(OpsInspectionRecordDO::getInspectorId, pageReqVO.getInspectorId())
.eqIfPresent(OpsInspectionRecordDO::getResultStatus, pageReqVO.getResultStatus())
.betweenIfPresent(OpsInspectionRecordDO::getCreateTime, pageReqVO.getCreateTime())
.orderByDesc(OpsInspectionRecordDO::getId));
}
@Override
public InspectionStatsRespVO getInspectionStats(InspectionStatsReqVO reqVO) {
// 1. 构建基础查询条件
LambdaQueryWrapperX<OpsInspectionRecordDO> baseWrapper = new LambdaQueryWrapperX<OpsInspectionRecordDO>()
.eqIfPresent(OpsInspectionRecordDO::getAreaId, reqVO.getAreaId())
.betweenIfPresent(OpsInspectionRecordDO::getCreateTime, reqVO.getCreateTime());
// 2. 总数 & 合格数
long totalCount = inspectionRecordMapper.selectCount(baseWrapper);
long passedCount = inspectionRecordMapper.selectCount(new LambdaQueryWrapperX<OpsInspectionRecordDO>()
.eqIfPresent(OpsInspectionRecordDO::getAreaId, reqVO.getAreaId())
.betweenIfPresent(OpsInspectionRecordDO::getCreateTime, reqVO.getCreateTime())
.eq(OpsInspectionRecordDO::getResultStatus, RESULT_STATUS_PASSED));
long failedCount = totalCount - passedCount;
// 3. 合格率
double passRate = totalCount > 0
? BigDecimal.valueOf(passedCount * 100.0 / totalCount).setScale(2, RoundingMode.HALF_UP).doubleValue()
: 0.0;
// 4. 不合格热点区域按不合格次数降序取前10
QueryWrapper<OpsInspectionRecordDO> groupWrapper = new QueryWrapper<>();
groupWrapper.select("area_id AS areaId", "COUNT(*) AS failedCount");
groupWrapper.eq("result_status", RESULT_STATUS_FAILED);
groupWrapper.eq("deleted", false);
if (reqVO.getAreaId() != null) {
groupWrapper.eq("area_id", reqVO.getAreaId());
}
if (reqVO.getCreateTime() != null && reqVO.getCreateTime().length == 2) {
groupWrapper.between("create_time", reqVO.getCreateTime()[0], reqVO.getCreateTime()[1]);
}
groupWrapper.groupBy("area_id");
groupWrapper.orderByDesc("failedCount");
groupWrapper.last("LIMIT 10");
List<Map<String, Object>> maps = inspectionRecordMapper.selectMaps(groupWrapper);
List<InspectionStatsRespVO.AreaFailStat> hotSpots = maps.stream()
.map(m -> InspectionStatsRespVO.AreaFailStat.builder()
.areaId(((Number) m.get("areaId")).longValue())
.failedCount(((Number) m.get("failedCount")).longValue())
.build())
.toList();
return InspectionStatsRespVO.builder()
.totalCount(totalCount)
.passedCount(passedCount)
.failedCount(failedCount)
.passRate(passRate)
.hotSpotAreas(hotSpots)
.build();
}
}

View File

@@ -0,0 +1,22 @@
package com.viewsh.module.ops.environment.service.inspection;
/**
* 巡检整改工单 Service 接口
*
* 当巡检不合格时,自动创建整改工单:
* - source_type = INSPECTION
* - 对接现有保洁工单引擎
* - 回写 generated_order_id 到巡检记录
*/
public interface InspectionRectificationService {
/**
* 创建整改工单
*
* @param recordId 巡检记录ID
* @param areaId 区域ID
* @return 创建的工单ID
*/
Long createRectificationOrder(Long recordId, Long areaId);
}

View File

@@ -0,0 +1,72 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.environment.dal.dataobject.CleanOrderAutoCreateReqDTO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
import com.viewsh.module.ops.environment.dal.mysql.inspection.OpsInspectionRecordMapper;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderService;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.SourceTypeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
/**
* 巡检整改工单 Service 实现
*/
@Service
@Validated
@Slf4j
public class InspectionRectificationServiceImpl implements InspectionRectificationService {
/** 默认整改工单预计时长(分钟) */
private static final int DEFAULT_RECTIFICATION_DURATION_MINUTES = 30;
@Resource
private CleanOrderService cleanOrderService;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private OpsInspectionRecordMapper inspectionRecordMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRectificationOrder(Long recordId, Long areaId) {
// 1. 获取区域信息
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
String areaName = (area != null) ? area.getAreaName() : "未知区域";
int expectedDuration = (area != null && area.getStandardDuration() != null)
? area.getStandardDuration() : DEFAULT_RECTIFICATION_DURATION_MINUTES;
// 2. 构建整改工单请求
CleanOrderAutoCreateReqDTO createReq = new CleanOrderAutoCreateReqDTO();
createReq.setOrderType("CLEAN");
createReq.setSourceType(SourceTypeEnum.INSPECTION.getType());
createReq.setTitle("巡检整改:" + areaName);
createReq.setDescription("巡检发现不合格需重新清洁。巡检记录ID" + recordId);
createReq.setPriority(PriorityEnum.P1.getPriority());
createReq.setAreaId(areaId);
createReq.setExpectedDuration(expectedDuration);
createReq.setCleaningType("SPOT");
createReq.setDifficultyLevel(3);
// 3. 调用现有工单引擎创建工单
Long orderId = cleanOrderService.createAutoCleanOrder(createReq);
log.info("[createRectificationOrder] 整改工单创建成功: recordId={}, areaId={}, orderId={}",
recordId, areaId, orderId);
// 4. 回写 generated_order_id 到巡检记录
OpsInspectionRecordDO update = new OpsInspectionRecordDO();
update.setId(recordId);
update.setGeneratedOrderId(orderId);
inspectionRecordMapper.updateById(update);
return orderId;
}
}

View File

@@ -0,0 +1,71 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplateSaveReqVO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionTemplateDO;
import java.util.List;
/**
* 巡检检查项模板 Service
*/
public interface InspectionTemplateService {
/**
* 创建巡检模板
*
* @param createReqVO 创建请求
* @return 模板ID
*/
Long createTemplate(InspectionTemplateSaveReqVO createReqVO);
/**
* 更新巡检模板
*
* @param updateReqVO 更新请求
*/
void updateTemplate(InspectionTemplateSaveReqVO updateReqVO);
/**
* 删除巡检模板
*
* @param id 模板ID
*/
void deleteTemplate(Long id);
/**
* 获得巡检模板
*
* @param id 模板ID
* @return 巡检模板
*/
OpsInspectionTemplateDO getTemplate(Long id);
/**
* 获得巡检模板分页
*
* @param pageReqVO 分页查询
* @return 巡检模板分页
*/
PageResult<OpsInspectionTemplateDO> getTemplatePage(InspectionTemplatePageReqVO pageReqVO);
/**
* 按功能类型获取启用的检查项列表
*
* @param functionType 功能类型
* @return 检查项列表(按 sortOrder 排序)
*/
List<OpsInspectionTemplateDO> getTemplateListByFunctionType(String functionType);
/**
* 按区域ID获取动态巡检表单检查项列表
* <p>
* 流程: area_id → 查 ops_bus_area 获取 function_type → 查 ops_inspection_template 加载检查项
*
* @param areaId 区域ID
* @return 检查项列表(按 sortOrder 排序)
*/
List<OpsInspectionTemplateDO> getTemplateListByAreaId(Long areaId);
}

View File

@@ -0,0 +1,104 @@
package com.viewsh.module.ops.environment.service.inspection;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.common.util.object.BeanUtils;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplateSaveReqVO;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionTemplateDO;
import com.viewsh.module.ops.environment.dal.mysql.inspection.OpsInspectionTemplateMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collections;
import java.util.List;
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.viewsh.module.ops.enums.ErrorCodeConstants.AREA_NOT_FOUND;
import static com.viewsh.module.ops.enums.ErrorCodeConstants.INSPECTION_TEMPLATE_NOT_FOUND;
/**
* 巡检检查项模板 Service 实现
*/
@Service
@Validated
public class InspectionTemplateServiceImpl implements InspectionTemplateService {
@Resource
private OpsInspectionTemplateMapper inspectionTemplateMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTemplate(InspectionTemplateSaveReqVO createReqVO) {
OpsInspectionTemplateDO template = BeanUtils.toBean(createReqVO, OpsInspectionTemplateDO.class);
if (template.getIsActive() == null) {
template.setIsActive(true);
}
inspectionTemplateMapper.insert(template);
return template.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateTemplate(InspectionTemplateSaveReqVO updateReqVO) {
if (updateReqVO.getId() == null) {
throw exception(INSPECTION_TEMPLATE_NOT_FOUND);
}
validateTemplateExists(updateReqVO.getId());
OpsInspectionTemplateDO updateObj = BeanUtils.toBean(updateReqVO, OpsInspectionTemplateDO.class);
inspectionTemplateMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteTemplate(Long id) {
validateTemplateExists(id);
inspectionTemplateMapper.deleteById(id);
}
@Override
public OpsInspectionTemplateDO getTemplate(Long id) {
return inspectionTemplateMapper.selectById(id);
}
@Override
public PageResult<OpsInspectionTemplateDO> getTemplatePage(InspectionTemplatePageReqVO pageReqVO) {
return inspectionTemplateMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<OpsInspectionTemplateDO>()
.eqIfPresent(OpsInspectionTemplateDO::getFunctionType, pageReqVO.getFunctionType())
.eqIfPresent(OpsInspectionTemplateDO::getIsActive, pageReqVO.getIsActive())
.likeIfPresent(OpsInspectionTemplateDO::getItemTitle, pageReqVO.getItemTitle())
.orderByAsc(OpsInspectionTemplateDO::getSortOrder));
}
@Override
public List<OpsInspectionTemplateDO> getTemplateListByFunctionType(String functionType) {
return inspectionTemplateMapper.selectListByFunctionType(functionType);
}
@Override
public List<OpsInspectionTemplateDO> getTemplateListByAreaId(Long areaId) {
OpsBusAreaDO area = opsBusAreaMapper.selectById(areaId);
if (area == null) {
throw exception(AREA_NOT_FOUND);
}
String functionType = area.getFunctionType();
if (functionType == null || functionType.isEmpty()) {
return Collections.emptyList();
}
return inspectionTemplateMapper.selectListByFunctionType(functionType);
}
private void validateTemplateExists(Long id) {
if (inspectionTemplateMapper.selectById(id) == null) {
throw exception(INSPECTION_TEMPLATE_NOT_FOUND);
}
}
}

View File

@@ -29,4 +29,8 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_RELATION_NOT_FOUND = new ErrorCode(1_020_002_003, "设备关联关系不存在");
ErrorCode IOT_SERVICE_UNAVAILABLE = new ErrorCode(1_020_002_004, "IoT 设备服务不可用,请稍后重试");
// ========== 巡检模块 1-020-003-000 ============
ErrorCode INSPECTION_TEMPLATE_NOT_FOUND = new ErrorCode(1_020_003_000, "巡检模板不存在");
ErrorCode INSPECTION_RECORD_NOT_FOUND = new ErrorCode(1_020_003_001, "巡检记录不存在");
}

View File

@@ -0,0 +1,72 @@
package com.viewsh.module.ops.controller.admin.inspection;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.common.util.object.BeanUtils;
import com.viewsh.framework.security.core.util.SecurityFrameworkUtils;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.*;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionRecordDO;
import com.viewsh.module.ops.environment.service.inspection.InspectionLocationService;
import com.viewsh.module.ops.environment.service.inspection.InspectionRecordService;
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;
@Resource
private InspectionRecordService inspectionRecordService;
@PostMapping("/verify-location")
@Operation(summary = "蓝牙位置校验")
@Parameter(name = "areaId", description = "区域ID", required = true)
@PreAuthorize("@ss.hasPermission('ops:inspection:create')")
public CommonResult<LocationVerifyResultVO> verifyLocation(
@RequestParam("areaId") Long areaId,
@Valid @RequestBody List<DetectedBeaconVO> detectedBeacons) {
return success(inspectionLocationService.verifyLocation(areaId, detectedBeacons));
}
@PostMapping("/submit")
@Operation(summary = "提交巡检结果")
@PreAuthorize("@ss.hasPermission('ops:inspection:create')")
public CommonResult<Long> submitInspection(@Valid @RequestBody InspectionSubmitReqVO submitReqVO) {
Long inspectorId = SecurityFrameworkUtils.getLoginUserId();
return success(inspectionRecordService.submitInspection(submitReqVO, inspectorId));
}
@GetMapping("/record/page")
@Operation(summary = "获得巡检记录分页")
@PreAuthorize("@ss.hasPermission('ops:inspection:query')")
public CommonResult<PageResult<InspectionRecordRespVO>> getRecordPage(@Valid InspectionRecordPageReqVO pageReqVO) {
PageResult<OpsInspectionRecordDO> pageResult = inspectionRecordService.getRecordPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InspectionRecordRespVO.class));
}
@GetMapping("/record/stats")
@Operation(summary = "获得巡检统计(合格率、不合格热点区域)")
@PreAuthorize("@ss.hasPermission('ops:inspection:query')")
public CommonResult<InspectionStatsRespVO> getInspectionStats(@Valid InspectionStatsReqVO reqVO) {
return success(inspectionRecordService.getInspectionStats(reqVO));
}
}

View File

@@ -0,0 +1,86 @@
package com.viewsh.module.ops.controller.admin.inspection;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.framework.common.pojo.PageResult;
import com.viewsh.framework.common.util.object.BeanUtils;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplatePageReqVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplateRespVO;
import com.viewsh.module.ops.environment.controller.admin.inspection.vo.InspectionTemplateSaveReqVO;
import com.viewsh.module.ops.environment.dal.dataobject.inspection.OpsInspectionTemplateDO;
import com.viewsh.module.ops.environment.service.inspection.InspectionTemplateService;
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/template")
@Validated
public class InspectionTemplateController {
@Resource
private InspectionTemplateService inspectionTemplateService;
@PostMapping("/create")
@Operation(summary = "创建巡检模板")
@PreAuthorize("@ss.hasPermission('ops:inspection-template:create')")
public CommonResult<Long> createTemplate(@Valid @RequestBody InspectionTemplateSaveReqVO createReqVO) {
return success(inspectionTemplateService.createTemplate(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新巡检模板")
@PreAuthorize("@ss.hasPermission('ops:inspection-template:update')")
public CommonResult<Boolean> updateTemplate(@Valid @RequestBody InspectionTemplateSaveReqVO updateReqVO) {
inspectionTemplateService.updateTemplate(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除巡检模板")
@Parameter(name = "id", description = "模板ID", required = true)
@PreAuthorize("@ss.hasPermission('ops:inspection-template:delete')")
public CommonResult<Boolean> deleteTemplate(@RequestParam("id") Long id) {
inspectionTemplateService.deleteTemplate(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得巡检模板")
@Parameter(name = "id", description = "模板ID", required = true)
@PreAuthorize("@ss.hasPermission('ops:inspection-template:query')")
public CommonResult<InspectionTemplateRespVO> getTemplate(@RequestParam("id") Long id) {
OpsInspectionTemplateDO template = inspectionTemplateService.getTemplate(id);
return success(BeanUtils.toBean(template, InspectionTemplateRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得巡检模板分页")
@PreAuthorize("@ss.hasPermission('ops:inspection-template:query')")
public CommonResult<PageResult<InspectionTemplateRespVO>> getTemplatePage(@Valid InspectionTemplatePageReqVO pageReqVO) {
PageResult<OpsInspectionTemplateDO> pageResult = inspectionTemplateService.getTemplatePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InspectionTemplateRespVO.class));
}
@GetMapping("/list-by-area")
@Operation(summary = "按区域获取巡检检查项列表(动态表单)")
@Parameter(name = "areaId", description = "区域ID", required = true)
@PreAuthorize("@ss.hasPermission('ops:inspection-template:query')")
public CommonResult<List<InspectionTemplateRespVO>> getTemplateListByArea(@RequestParam("areaId") Long areaId) {
List<OpsInspectionTemplateDO> list = inspectionTemplateService.getTemplateListByAreaId(areaId);
return success(BeanUtils.toBean(list, InspectionTemplateRespVO.class));
}
}