|
|
@@ -0,0 +1,153 @@
|
|
|
+package com.zksy.web.websocket;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.alibaba.fastjson.serializer.SerializerFeature;
|
|
|
+import com.zksy.base.domain.vo.WorkOrderPushVO;
|
|
|
+import com.zksy.base.service.BusinessWorkOrderService;
|
|
|
+import com.zksy.common.core.domain.model.LoginUser;
|
|
|
+import com.zksy.common.utils.DateUtils;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.socket.WebSocketSession;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 待处理及超时工单 WebSocket 主动推送任务。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class WorkOrderWebSocketPushTask {
|
|
|
+
|
|
|
+ public static final String PUSH_TYPE = "workOrderReminderPush";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private BusinessWorkOrderService businessWorkOrderService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private DeviceStatusWebSocketHandler webSocketHandler;
|
|
|
+
|
|
|
+ @Value("${websocket.work-order.max-items:100}")
|
|
|
+ private int maxItems;
|
|
|
+
|
|
|
+ /** sessionId -> 上次已推送的业务快照指纹。 */
|
|
|
+ private final Map<String, String> lastFingerprints = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ @Scheduled(
|
|
|
+ fixedDelayString = "${websocket.work-order.scan-interval-ms:60000}",
|
|
|
+ initialDelayString = "${websocket.work-order.initial-delay-ms:10000}")
|
|
|
+ public void pushPendingOrOverdueWorkOrders() {
|
|
|
+ Map<String, WebSocketSession> sessions = webSocketHandler.getSessionSnapshot();
|
|
|
+ cleanupDisconnectedSessions(sessions.keySet());
|
|
|
+ if (sessions.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先完成会话鉴权,再查库;只有确实存在需要接收工单提醒的用户时才执行扫描。
|
|
|
+ // 这样设备状态专用连接不会触发无意义的工单全表查询,同时每次扫描只校验一次 Token。
|
|
|
+ Map<String, LoginUser> authenticatedUsers = new HashMap<>();
|
|
|
+ for (Map.Entry<String, WebSocketSession> entry : sessions.entrySet()) {
|
|
|
+ LoginUser loginUser = webSocketHandler.getAuthenticatedUser(entry.getValue());
|
|
|
+ if (loginUser == null) {
|
|
|
+ lastFingerprints.remove(entry.getKey());
|
|
|
+ } else {
|
|
|
+ authenticatedUsers.put(entry.getKey(), loginUser);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (authenticatedUsers.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ List<WorkOrderPushVO> candidates = businessWorkOrderService.selectPendingOrOverdueWorkOrders(now);
|
|
|
+ if (candidates == null) {
|
|
|
+ candidates = Collections.emptyList();
|
|
|
+ }
|
|
|
+ for (Map.Entry<String, LoginUser> userEntry : authenticatedUsers.entrySet()) {
|
|
|
+ String sessionId = userEntry.getKey();
|
|
|
+ WebSocketSession session = sessions.get(sessionId);
|
|
|
+ LoginUser loginUser = userEntry.getValue();
|
|
|
+
|
|
|
+ List<WorkOrderPushVO> visibleItems = candidates.stream()
|
|
|
+ .filter(item -> isVisibleTo(item, loginUser))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ pushSnapshot(sessionId, session, loginUser, visibleItems, now);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void pushSnapshot(String sessionId, WebSocketSession session,
|
|
|
+ LoginUser loginUser, List<WorkOrderPushVO> visibleItems, Date now) {
|
|
|
+ int total = visibleItems.size();
|
|
|
+ int limit = Math.max(1, maxItems);
|
|
|
+ List<WorkOrderPushVO> items = total > limit
|
|
|
+ ? new ArrayList<>(visibleItems.subList(0, limit)) : visibleItems;
|
|
|
+ String fingerprint = buildFingerprint(visibleItems, loginUser);
|
|
|
+ String previous = lastFingerprints.get(sessionId);
|
|
|
+
|
|
|
+ if (fingerprint.equals(previous) || (previous == null && visibleItems.isEmpty())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject data = new JSONObject();
|
|
|
+ data.put("items", items);
|
|
|
+ data.put("total", total);
|
|
|
+ data.put("truncated", total > items.size());
|
|
|
+ data.put("serverTime", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, now));
|
|
|
+
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", PUSH_TYPE);
|
|
|
+ response.put("success", true);
|
|
|
+ response.put("data", data);
|
|
|
+ String payload = JSON.toJSONString(response, SerializerFeature.WriteDateUseDateFormat);
|
|
|
+ if (webSocketHandler.sendText(session, payload)) {
|
|
|
+ lastFingerprints.put(sessionId, fingerprint);
|
|
|
+ log.info("工单提醒推送完成: sessionId={}, total={}, sent={}",
|
|
|
+ sessionId, total, items.size());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean isVisibleTo(WorkOrderPushVO item, LoginUser loginUser) {
|
|
|
+ return DeviceStatusWebSocketHandler.isWorkOrderVisibleTo(item, loginUser);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildFingerprint(List<WorkOrderPushVO> items, LoginUser loginUser) {
|
|
|
+ if (items.isEmpty()) {
|
|
|
+ return "EMPTY:" + userFingerprint(loginUser);
|
|
|
+ }
|
|
|
+ return userFingerprint(loginUser) + ":" + items.stream()
|
|
|
+ .map(item -> String.valueOf(item.getOrderId()) + ':'
|
|
|
+ + item.getOrderStatus() + ':'
|
|
|
+ + time(item.getEffectiveDeadline()) + ':'
|
|
|
+ + time(item.getUpdateTime()) + ':'
|
|
|
+ + item.getDeptId() + ':'
|
|
|
+ + item.getReceiveUser())
|
|
|
+ .collect(Collectors.joining("|"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String userFingerprint(LoginUser loginUser) {
|
|
|
+ if (loginUser == null) {
|
|
|
+ return "ANONYMOUS";
|
|
|
+ }
|
|
|
+ return String.valueOf(loginUser.getUserId()) + ":" + String.valueOf(loginUser.getDeptId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private long time(Date value) {
|
|
|
+ return value != null ? value.getTime() : 0L;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void cleanupDisconnectedSessions(Set<String> activeSessionIds) {
|
|
|
+ lastFingerprints.keySet().retainAll(activeSessionIds);
|
|
|
+ }
|
|
|
+}
|