16 changed files with 1151 additions and 75 deletions
@ -0,0 +1,219 @@ |
|||
package com.ruoyi.websocket.service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Comparator; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.messaging.simp.SimpMessagingTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 房间内用户「当前操作」聚合:正在编辑的对象、地图选中项等。内存存储,随会话清理。 |
|||
*/ |
|||
@Service |
|||
public class RoomUserActivityService { |
|||
|
|||
private static final String TYPE_USER_ACTIVITIES = "USER_ACTIVITIES"; |
|||
|
|||
/** roomId -> sessionId -> activity row */ |
|||
private final ConcurrentHashMap<Long, ConcurrentHashMap<String, Map<String, Object>>> byRoom = |
|||
new ConcurrentHashMap<>(); |
|||
|
|||
@Autowired |
|||
private SimpMessagingTemplate messagingTemplate; |
|||
|
|||
public void onEditLock(Long roomId, String sessionId, Map<String, Object> editor, |
|||
String objectType, Object objectId, String objectLabel) { |
|||
if (roomId == null || sessionId == null || objectType == null || objectId == null) { |
|||
return; |
|||
} |
|||
String label = objectLabel != null ? objectLabel : ""; |
|||
Map<String, Object> row = getOrCreateRow(roomId, sessionId, editor); |
|||
Map<String, Object> editing = new HashMap<>(); |
|||
editing.put("objectType", objectType); |
|||
editing.put("objectId", objectId); |
|||
editing.put("objectLabel", label); |
|||
editing.put("startedAt", System.currentTimeMillis()); |
|||
row.put("editing", editing); |
|||
} |
|||
|
|||
public void onEditUnlock(Long roomId, String sessionId, String objectType, Object objectId) { |
|||
if (roomId == null || sessionId == null || objectType == null || objectId == null) { |
|||
return; |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> row = m.get(sessionId); |
|||
if (row == null) { |
|||
return; |
|||
} |
|||
@SuppressWarnings("unchecked") |
|||
Map<String, Object> editing = (Map<String, Object>) row.get("editing"); |
|||
if (editing == null) { |
|||
return; |
|||
} |
|||
if (objectType.equals(String.valueOf(editing.get("objectType"))) |
|||
&& Objects.equals(objectId, editing.get("objectId"))) { |
|||
row.remove("editing"); |
|||
} |
|||
cleanupRow(roomId, sessionId, row, m); |
|||
} |
|||
|
|||
public void onSelection(Long roomId, String sessionId, Map<String, Object> editor, |
|||
String summary, int count, List<Map<String, Object>> items) { |
|||
if (roomId == null || sessionId == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> row = getOrCreateRow(roomId, sessionId, editor); |
|||
if (count <= 0 || items == null || items.isEmpty()) { |
|||
row.remove("selection"); |
|||
} else { |
|||
Map<String, Object> sel = new HashMap<>(); |
|||
sel.put("summary", summary != null ? summary : ""); |
|||
sel.put("count", count); |
|||
sel.put("items", new ArrayList<>(items)); |
|||
sel.put("updatedAt", System.currentTimeMillis()); |
|||
row.put("selection", sel); |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m != null) { |
|||
cleanupRow(roomId, sessionId, row, m); |
|||
} |
|||
} |
|||
|
|||
public void removeSession(Long roomId, String sessionId) { |
|||
if (roomId == null || sessionId == null) { |
|||
return; |
|||
} |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m != null) { |
|||
m.remove(sessionId); |
|||
if (m.isEmpty()) { |
|||
byRoom.remove(roomId); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public List<Map<String, Object>> snapshot(Long roomId) { |
|||
ConcurrentHashMap<String, Map<String, Object>> m = byRoom.get(roomId); |
|||
if (m == null || m.isEmpty()) { |
|||
return new ArrayList<>(); |
|||
} |
|||
List<Map<String, Object>> out = new ArrayList<>(); |
|||
for (Map<String, Object> row : m.values()) { |
|||
Map<String, Object> packed = packRowForClient(row); |
|||
if (packed != null) { |
|||
out.add(packed); |
|||
} |
|||
} |
|||
out.sort(Comparator.comparing(a -> editorSortKey(a.get("editor")))); |
|||
return out; |
|||
} |
|||
|
|||
public void broadcastSnapshot(Long roomId) { |
|||
if (roomId == null) { |
|||
return; |
|||
} |
|||
Map<String, Object> msg = new HashMap<>(); |
|||
msg.put("type", TYPE_USER_ACTIVITIES); |
|||
msg.put("activities", snapshot(roomId)); |
|||
messagingTemplate.convertAndSend("/topic/room/" + roomId, msg); |
|||
} |
|||
|
|||
public void sendSnapshotToUser(Long roomId, String userName) { |
|||
if (roomId == null || userName == null || userName.isEmpty()) { |
|||
return; |
|||
} |
|||
Map<String, Object> msg = new HashMap<>(); |
|||
msg.put("type", TYPE_USER_ACTIVITIES); |
|||
msg.put("activities", snapshot(roomId)); |
|||
messagingTemplate.convertAndSendToUser(userName, "/queue/private", msg); |
|||
} |
|||
|
|||
private static String editorSortKey(Object editorObj) { |
|||
if (!(editorObj instanceof Map)) { |
|||
return ""; |
|||
} |
|||
Map<?, ?> ed = (Map<?, ?>) editorObj; |
|||
Object nn = ed.get("nickName"); |
|||
return nn != null ? String.valueOf(nn) : ""; |
|||
} |
|||
|
|||
private Map<String, Object> getOrCreateRow(Long roomId, String sessionId, Map<String, Object> editor) { |
|||
ConcurrentHashMap<String, Map<String, Object>> m = |
|||
byRoom.computeIfAbsent(roomId, k -> new ConcurrentHashMap<>()); |
|||
return m.compute(sessionId, (sid, old) -> { |
|||
if (old != null) { |
|||
if (editor != null) { |
|||
old.put("editor", new HashMap<>(editor)); |
|||
} |
|||
return old; |
|||
} |
|||
Map<String, Object> row = new HashMap<>(); |
|||
row.put("sessionId", sessionId); |
|||
if (editor != null) { |
|||
row.put("editor", new HashMap<>(editor)); |
|||
} |
|||
return row; |
|||
}); |
|||
} |
|||
|
|||
private void cleanupRow(Long roomId, String sessionId, Map<String, Object> row, |
|||
ConcurrentHashMap<String, Map<String, Object>> m) { |
|||
boolean hasEdit = row.containsKey("editing"); |
|||
boolean hasSel = false; |
|||
if (row.containsKey("selection")) { |
|||
Object selObj = row.get("selection"); |
|||
if (selObj instanceof Map) { |
|||
Map<?, ?> sel = (Map<?, ?>) selObj; |
|||
Object c = sel.get("count"); |
|||
int cnt = c instanceof Number ? ((Number) c).intValue() : 0; |
|||
if (cnt > 0) { |
|||
hasSel = true; |
|||
} else { |
|||
row.remove("selection"); |
|||
} |
|||
} else { |
|||
row.remove("selection"); |
|||
} |
|||
} |
|||
if (!hasEdit && !hasSel) { |
|||
m.remove(sessionId); |
|||
} |
|||
if (m.isEmpty()) { |
|||
byRoom.remove(roomId); |
|||
} |
|||
} |
|||
|
|||
private Map<String, Object> packRowForClient(Map<String, Object> row) { |
|||
boolean hasEdit = row.containsKey("editing"); |
|||
boolean hasSel = false; |
|||
if (row.containsKey("selection")) { |
|||
Object selObj = row.get("selection"); |
|||
if (selObj instanceof Map) { |
|||
Map<?, ?> sel = (Map<?, ?>) selObj; |
|||
Object c = sel.get("count"); |
|||
hasSel = c instanceof Number && ((Number) c).intValue() > 0; |
|||
} |
|||
} |
|||
if (!hasEdit && !hasSel) { |
|||
return null; |
|||
} |
|||
Map<String, Object> copy = new HashMap<>(); |
|||
copy.put("sessionId", row.get("sessionId")); |
|||
copy.put("editor", row.get("editor")); |
|||
if (hasEdit) { |
|||
copy.put("editing", row.get("editing")); |
|||
} |
|||
if (hasSel) { |
|||
copy.put("selection", row.get("selection")); |
|||
} |
|||
return copy; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue