diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainService.java
new file mode 100644
index 0000000..0641685
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainService.java
@@ -0,0 +1,370 @@
+package com.viewsh.module.ops.environment.domain.cleaner;
+
+import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
+import com.viewsh.module.ops.environment.integration.adapter.CleanerAssigneeStatusAdapter;
+import com.viewsh.module.ops.enums.PriorityEnum;
+
+import java.util.List;
+
+/**
+ * 保洁员领域服务接口
+ *
+ * 职责:
+ * 1. 保洁员状态管理
+ * 2. 保洁员可用性查询
+ * 3. 保洁员工作量统计
+ *
+ * 设计说明:
+ * - 业务层领域服务,处理保洁业务特定逻辑
+ * - 通过 {@link AssigneeStatus} 接口与通用调度引擎解耦
+ *
+ * @author lzh
+ */
+public interface CleanerDomainService {
+
+ // ==================== 状态查询 ====================
+
+ /**
+ * 获取保洁员状态(通用接口)
+ *
+ * @param userId 保洁员ID
+ * @return 通用执行人状态接口
+ */
+ AssigneeStatus getAssigneeStatus(Long userId);
+
+ /**
+ * 获取保洁员当前工作量
+ *
+ * @param userId 保洁员ID
+ * @return 工作量信息
+ */
+ CleanerWorkload getWorkload(Long userId);
+
+ /**
+ * 检查保洁员是否可用(可接新单)
+ *
+ * @param userId 保洁员ID
+ * @return 是否可用
+ */
+ boolean isAvailable(Long userId);
+
+ /**
+ * 检查保洁员是否在线
+ *
+ * @param userId 保洁员ID
+ * @return 是否在线
+ */
+ boolean isOnline(Long userId);
+
+ // ==================== 保洁员查询 ====================
+
+ /**
+ * 查询指定区域的保洁员列表
+ *
+ * @param areaId 区域ID
+ * @return 保洁员列表(通用状态接口)
+ */
+ List listByArea(Long areaId);
+
+ /**
+ * 查询指定区域的可用保洁员
+ *
+ * @param areaId 区域ID
+ * @return 可用保洁员列表
+ */
+ List listAvailableByArea(Long areaId);
+
+ /**
+ * 查询附近的保洁员
+ *
+ * @param areaId 中心区域ID
+ * @param radius 搜索半径
+ * @return 附近保洁员列表
+ */
+ List listNearby(Long areaId, Integer radius);
+
+ /**
+ * 根据条件推荐保洁员
+ *
+ * @param areaId 区域ID
+ * @param priority 工单优先级
+ * @param limit 最多返回数量
+ * @return 推荐的保洁员列表
+ */
+ List recommend(Long areaId, PriorityEnum priority, int limit);
+
+ /**
+ * 获取最佳保洁员
+ *
+ * @param areaId 区域ID
+ * @param priority 工单优先级
+ * @return 推荐结果,无合适保洁员返回null
+ */
+ CleanerRecommendation getBestCleaner(Long areaId, PriorityEnum priority);
+
+ // ==================== 工作量统计 ====================
+
+ /**
+ * 统计保洁员今日完成任务数
+ *
+ * @param userId 保洁员ID
+ * @return 完成任务数
+ */
+ int countCompletedToday(Long userId);
+
+ /**
+ * 统计保洁员今日工作时长(分钟)
+ *
+ * @param userId 保洁员ID
+ * @return 工作时长
+ */
+ int getWorkMinutesToday(Long userId);
+
+ /**
+ * 获取保洁员当前等待任务数
+ *
+ * @param userId 保洁员ID
+ * @return 等待任务数
+ */
+ int getWaitingTaskCount(Long userId);
+
+ // ==================== 状态更新(仅业务层调用)====================
+
+ /**
+ * 设置保洁员当前工单
+ *
+ * @param userId 保洁员ID
+ * @param orderId 工单ID
+ * @param orderCode 工单编号
+ */
+ void setCurrentWorkOrder(Long userId, Long orderId, String orderCode);
+
+ /**
+ * 清理保洁员当前工单
+ *
+ * @param userId 保洁员ID
+ */
+ void clearCurrentWorkOrder(Long userId);
+
+ /**
+ * 更新保洁员当前位置
+ *
+ * @param userId 保洁员ID
+ * @param areaId 区域ID
+ * @param areaName 区域名称
+ */
+ void updateCurrentArea(Long userId, Long areaId, String areaName);
+
+ // ==================== 内部类 ====================
+
+ /**
+ * 保洁员工作量信息
+ */
+ class CleanerWorkload {
+ /**
+ * 保洁员ID
+ */
+ private Long userId;
+
+ /**
+ * 保洁员姓名
+ */
+ private String userName;
+
+ /**
+ * 当前任务数
+ */
+ private int currentTaskCount;
+
+ /**
+ * 等待任务数
+ */
+ private int waitingTaskCount;
+
+ /**
+ * 今日完成任务数
+ */
+ private int completedToday;
+
+ /**
+ * 今日工作时长(分钟)
+ */
+ private int workMinutesToday;
+
+ public CleanerWorkload() {
+ }
+
+ public CleanerWorkload(Long userId, String userName, int currentTaskCount,
+ int waitingTaskCount, int completedToday, int workMinutesToday) {
+ this.userId = userId;
+ this.userName = userName;
+ this.currentTaskCount = currentTaskCount;
+ this.waitingTaskCount = waitingTaskCount;
+ this.completedToday = completedToday;
+ this.workMinutesToday = workMinutesToday;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public int getCurrentTaskCount() {
+ return currentTaskCount;
+ }
+
+ public void setCurrentTaskCount(int currentTaskCount) {
+ this.currentTaskCount = currentTaskCount;
+ }
+
+ public int getWaitingTaskCount() {
+ return waitingTaskCount;
+ }
+
+ public void setWaitingTaskCount(int waitingTaskCount) {
+ this.waitingTaskCount = waitingTaskCount;
+ }
+
+ public int getCompletedToday() {
+ return completedToday;
+ }
+
+ public void setCompletedToday(int completedToday) {
+ this.completedToday = completedToday;
+ }
+
+ public int getWorkMinutesToday() {
+ return workMinutesToday;
+ }
+
+ public void setWorkMinutesToday(int workMinutesToday) {
+ this.workMinutesToday = workMinutesToday;
+ }
+
+ /**
+ * 获取总任务数(当前+等待)
+ */
+ public int getTotalTaskCount() {
+ return currentTaskCount + waitingTaskCount;
+ }
+
+ /**
+ * 是否忙碌
+ */
+ public boolean isBusy() {
+ return currentTaskCount > 0;
+ }
+ }
+
+ /**
+ * 保洁员推荐结果
+ */
+ class CleanerRecommendation {
+ /**
+ * 保洁员ID
+ */
+ private Long userId;
+
+ /**
+ * 保洁员姓名
+ */
+ private String userName;
+
+ /**
+ * 推荐分数(0-100)
+ */
+ private int score;
+
+ /**
+ * 推荐理由
+ */
+ private String reason;
+
+ /**
+ * 电量
+ */
+ private Integer batteryLevel;
+
+ /**
+ * 状态
+ */
+ private String status;
+
+ public CleanerRecommendation() {
+ }
+
+ public CleanerRecommendation(Long userId, String userName, int score, String reason) {
+ this.userId = userId;
+ this.userName = userName;
+ this.score = score;
+ this.reason = reason;
+ }
+
+ public static CleanerRecommendation of(Long userId, String userName, int score, String reason) {
+ return new CleanerRecommendation(userId, userName, score, reason);
+ }
+
+ public static CleanerRecommendation none() {
+ return null;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public int getScore() {
+ return score;
+ }
+
+ public void setScore(int score) {
+ this.score = score;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public void setReason(String reason) {
+ this.reason = reason;
+ }
+
+ public Integer getBatteryLevel() {
+ return batteryLevel;
+ }
+
+ public void setBatteryLevel(Integer batteryLevel) {
+ this.batteryLevel = batteryLevel;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+ }
+}
diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainServiceImpl.java
new file mode 100644
index 0000000..e770283
--- /dev/null
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/domain/cleaner/CleanerDomainServiceImpl.java
@@ -0,0 +1,325 @@
+package com.viewsh.module.ops.environment.domain.cleaner;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.viewsh.module.ops.api.queue.OrderQueueDTO;
+import com.viewsh.module.ops.api.queue.OrderQueueService;
+import com.viewsh.module.ops.core.dispatch.model.AssigneeStatus;
+import com.viewsh.module.ops.environment.dal.dataobject.cleaner.OpsCleanerStatusDO;
+import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
+import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
+import com.viewsh.module.ops.enums.CleanerStatusEnum;
+import com.viewsh.module.ops.enums.PriorityEnum;
+import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
+import com.viewsh.module.ops.environment.integration.adapter.CleanerAssigneeStatusAdapter;
+import com.viewsh.module.ops.environment.service.cleaner.CleanerStatusService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 保洁员领域服务实现
+ *
+ * 职责:
+ * 1. 保洁员状态管理
+ * 2. 保洁员可用性查询
+ * 3. 保洁员工作量统计
+ *
+ * 设计说明:
+ * - 业务层领域服务,处理保洁业务特定逻辑
+ * - 通过 {@link AssigneeStatus} 接口与通用调度引擎解耦
+ * - 使用 {@link CleanerAssigneeStatusAdapter} 适配保洁员状态到通用接口
+ *
+ * @author lzh
+ */
+@Slf4j
+@Service
+public class CleanerDomainServiceImpl implements CleanerDomainService {
+
+ @Resource
+ private CleanerStatusService cleanerStatusService;
+
+ @Resource
+ private OrderQueueService orderQueueService;
+
+ @Resource
+ private OpsOrderMapper orderMapper;
+
+ // ==================== 状态查询 ====================
+
+ @Override
+ public AssigneeStatus getAssigneeStatus(Long userId) {
+ OpsCleanerStatusDO cleanerStatus = cleanerStatusService.getStatus(userId);
+ if (cleanerStatus == null) {
+ return null;
+ }
+
+ // 获取等待任务数
+ int waitingCount = getWaitingTaskCount(userId);
+
+ return new CleanerAssigneeStatusAdapter(cleanerStatus, (long) waitingCount);
+ }
+
+ @Override
+ public CleanerWorkload getWorkload(Long userId) {
+ OpsCleanerStatusDO cleanerStatus = cleanerStatusService.getStatus(userId);
+ if (cleanerStatus == null) {
+ return new CleanerWorkload();
+ }
+
+ // 获取等待任务数
+ int waitingCount = getWaitingTaskCount(userId);
+
+ // 统计今日完成任务数
+ int completedToday = countCompletedToday(userId);
+
+ // 统计今日工作时长
+ int workMinutes = getWorkMinutesToday(userId);
+
+ return new CleanerWorkload(
+ userId,
+ cleanerStatus.getUserName(),
+ cleanerStatus.getCurrentOpsOrderId() != null ? 1 : 0,
+ waitingCount,
+ completedToday,
+ workMinutes
+ );
+ }
+
+ @Override
+ public boolean isAvailable(Long userId) {
+ OpsCleanerStatusDO cleanerStatus = cleanerStatusService.getStatus(userId);
+ if (cleanerStatus == null) {
+ return false;
+ }
+
+ CleanerStatusEnum statusEnum = cleanerStatus.getStatusEnum();
+ return statusEnum != null && statusEnum.isCanAcceptNewOrder();
+ }
+
+ @Override
+ public boolean isOnline(Long userId) {
+ return cleanerStatusService.isOnline(userId);
+ }
+
+ // ==================== 保洁员查询 ====================
+
+ @Override
+ public List listByArea(Long areaId) {
+ List cleaners = cleanerStatusService.listCleanersByArea(areaId);
+
+ return cleaners.stream()
+ .map(cleaner -> {
+ int waitingCount = getWaitingTaskCount(cleaner.getUserId());
+ return new CleanerAssigneeStatusAdapter(cleaner, (long) waitingCount);
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List listAvailableByArea(Long areaId) {
+ List cleaners = cleanerStatusService.listAvailableCleaners(areaId);
+
+ return cleaners.stream()
+ .map(cleaner -> {
+ int waitingCount = getWaitingTaskCount(cleaner.getUserId());
+ return new CleanerAssigneeStatusAdapter(cleaner, (long) waitingCount);
+ })
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List listNearby(Long areaId, Integer radius) {
+ // TODO: 实现基于地理位置的附近查询
+ // 目前简化为按区域查询
+ return listByArea(areaId);
+ }
+
+ @Override
+ public List recommend(Long areaId, PriorityEnum priority, int limit) {
+ List cleaners = cleanerStatusService.listCleanersByArea(areaId);
+ if (cleaners.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ // 过滤在线的保洁员
+ List onlineCleaners = cleaners.stream()
+ .filter(c -> CleanerStatusEnum.OFFLINE != c.getStatusEnum())
+ .collect(Collectors.toList());
+
+ // 按分数排序
+ return onlineCleaners.stream()
+ .limit(limit)
+ .map(cleaner -> {
+ int score = calculateScore(cleaner);
+ String reason = buildRecommendationReason(cleaner);
+ return CleanerRecommendation.of(
+ cleaner.getUserId(),
+ cleaner.getUserName(),
+ score,
+ reason
+ );
+ })
+ .sorted((a, b) -> Integer.compare(b.getScore(), a.getScore()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public CleanerRecommendation getBestCleaner(Long areaId, PriorityEnum priority) {
+ List recommendations = recommend(areaId, priority, 1);
+ return recommendations.isEmpty() ? null : recommendations.get(0);
+ }
+
+ // ==================== 工作量统计 ====================
+
+ @Override
+ public int countCompletedToday(Long userId) {
+ LocalDate today = LocalDate.now();
+ LocalDateTime startOfDay = today.atStartOfDay();
+ LocalDateTime endOfDay = today.plusDays(1).atStartOfDay();
+
+ // 查询今日完成的保洁工单
+// LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+// queryWrapper.eq(OpsOrderDO::getAssigneeId, userId)
+// .eq(OpsOrderDO::getOrderType, "CLEAN")
+// .eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.COMPLETED.getStatus())
+// .ge(OpsOrderDO::getCompletionTime, startOfDay)
+// .lt(OpsOrderDO::getCompletionTime, endOfDay);
+
+// return orderMapper.selectCount(queryWrapper);
+ return 0;
+ }
+
+ @Override
+ public int getWorkMinutesToday(Long userId) {
+ LocalDate today = LocalDate.now();
+ LocalDateTime startOfDay = today.atStartOfDay();
+ LocalDateTime endOfDay = today.plusDays(1).atStartOfDay();
+
+ // 查询今日完成的保洁工单
+// LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+// queryWrapper.eq(OpsOrderDO::getAssigneeId, userId)
+// .eq(OpsOrderDO::getOrderType, "CLEAN")
+// .eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.COMPLETED.getStatus())
+// .ge(OpsOrderDO::getCompletionTime, startOfDay)
+// .lt(OpsOrderDO::getCompletionTime, endOfDay)
+// .isNotNull(OpsOrderDO::getCompletionTime)
+// .isNotNull(OpsOrderDO::getDispatchTime);
+
+// List orders = orderMapper.selectList(queryWrapper);
+
+ // 计算总工作时长(分钟)
+ int totalMinutes = 0;
+// for (OpsOrderDO order : orders) {
+// if (order.getDispatchTime() != null && order.getCompletionTime() != null) {
+// long minutes = Duration.between(order.getDispatchTime(), order.getCompletionTime()).toMinutes();
+// totalMinutes += minutes;
+// }
+// }
+
+ return totalMinutes;
+ }
+
+ @Override
+ public int getWaitingTaskCount(Long userId) {
+ try {
+ List waitingTasks = orderQueueService.getWaitingTasksByUserId(userId);
+ return waitingTasks != null ? waitingTasks.size() : 0;
+ } catch (Exception e) {
+ log.warn("查询等待任务数量失败: userId={}", userId, e);
+ return 0;
+ }
+ }
+
+ // ==================== 状态更新 ====================
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void setCurrentWorkOrder(Long userId, Long orderId, String orderCode) {
+ cleanerStatusService.setCurrentWorkOrder(userId, orderId, orderCode);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void clearCurrentWorkOrder(Long userId) {
+ cleanerStatusService.clearCurrentWorkOrder(userId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateCurrentArea(Long userId, Long areaId, String areaName) {
+ cleanerStatusService.updateCurrentArea(userId, areaId, areaName);
+ }
+
+ // ==================== 私有方法 ====================
+
+ /**
+ * 计算推荐分数(0-100)
+ */
+ private int calculateScore(OpsCleanerStatusDO cleaner) {
+ int score = 50; // 基础分
+
+ CleanerStatusEnum statusEnum = cleaner.getStatusEnum();
+
+ // 状态分数
+ if (statusEnum == CleanerStatusEnum.IDLE) {
+ score += 30;
+ } else if (statusEnum == CleanerStatusEnum.BUSY) {
+ score += 10;
+ }
+
+ // 电量分数
+ if (cleaner.getBatteryLevel() != null) {
+ if (cleaner.getBatteryLevel() > 80) {
+ score += 15;
+ } else if (cleaner.getBatteryLevel() > 50) {
+ score += 10;
+ } else if (cleaner.getBatteryLevel() > 20) {
+ score += 5;
+ }
+ }
+
+ // 心跳分数
+ if (cleaner.getLastHeartbeatTime() != null) {
+ long minutesSinceHeartbeat = Duration.between(
+ cleaner.getLastHeartbeatTime(),
+ LocalDateTime.now()
+ ).toMinutes();
+ if (minutesSinceHeartbeat < 5) {
+ score += 5;
+ }
+ }
+
+ return Math.min(score, 100);
+ }
+
+ /**
+ * 构建推荐理由
+ */
+ private String buildRecommendationReason(OpsCleanerStatusDO cleaner) {
+ StringBuilder reason = new StringBuilder();
+
+ CleanerStatusEnum statusEnum = cleaner.getStatusEnum();
+ if (statusEnum != null) {
+ reason.append("状态=").append(statusEnum.getDescription());
+ }
+
+ if (cleaner.getBatteryLevel() != null) {
+ reason.append("、电量").append(cleaner.getBatteryLevel()).append("%");
+ }
+
+ if (cleaner.getCurrentAreaName() != null) {
+ reason.append("、位置=").append(cleaner.getCurrentAreaName());
+ }
+
+ return reason.toString();
+ }
+}