4.29 多人协同编辑图片 websocket Disruptor
This commit is contained in:
parent
6a5fb45cc2
commit
528422fed6
11
pom.xml
11
pom.xml
@ -102,6 +102,17 @@
|
|||||||
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
|
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
|
||||||
<version>5.2.0</version>
|
<version>5.2.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- websocket -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 高性能无锁队列 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lmax</groupId>
|
||||||
|
<artifactId>disruptor</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
@ -13,8 +13,14 @@ public class MainController {
|
|||||||
* 健康检查
|
* 健康检查
|
||||||
*/
|
*/
|
||||||
@GetMapping("/health")
|
@GetMapping("/health")
|
||||||
public BaseResponse<String> health() {
|
public BaseResponse<String> health() throws InterruptedException {
|
||||||
|
// new Thread(() -> {
|
||||||
|
// try {
|
||||||
|
// Thread.sleep(100000L);
|
||||||
|
// } catch (InterruptedException e) {
|
||||||
|
// throw new RuntimeException(e);
|
||||||
|
// }
|
||||||
|
// }).start();
|
||||||
return ResultUtils.success("ok");
|
return ResultUtils.success("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ public class PictureController {
|
|||||||
// 查询数据库
|
// 查询数据库
|
||||||
Picture picture = pictureService.getById(id);
|
Picture picture = pictureService.getById(id);
|
||||||
ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);
|
ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||||
ThrowUtils.throwIf(PictureReviewStatusEnum.PASS.getValue()!=picture.getReviewStatus(),ErrorCode.NOT_FOUND_ERROR);
|
// ThrowUtils.throwIf(PictureReviewStatusEnum.PASS.getValue()!=picture.getReviewStatus(),ErrorCode.NOT_FOUND_ERROR);
|
||||||
// 空间权限校验
|
// 空间权限校验
|
||||||
Long spaceId = picture.getSpaceId();
|
Long spaceId = picture.getSpaceId();
|
||||||
Space space = null;
|
Space space = null;
|
||||||
@ -186,6 +186,8 @@ public class PictureController {
|
|||||||
//User loginUser = userService.getLoginUser(request);
|
//User loginUser = userService.getLoginUser(request);
|
||||||
//已经改为使用sa-token鉴权
|
//已经改为使用sa-token鉴权
|
||||||
//pictureService.checkPictureAuth(loginUser, picture);
|
//pictureService.checkPictureAuth(loginUser, picture);
|
||||||
|
space = spaceService.getById(spaceId);
|
||||||
|
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||||
}
|
}
|
||||||
// 获取权限列表
|
// 获取权限列表
|
||||||
User loginUser = userService.getLoginUser(request);
|
User loginUser = userService.getLoginUser(request);
|
||||||
@ -228,7 +230,7 @@ public class PictureController {
|
|||||||
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
|
||||||
pictureQueryRequest.setNullSpaceId(true);
|
pictureQueryRequest.setNullSpaceId(true);
|
||||||
} else {
|
} else {
|
||||||
// 私有空间
|
// 私有空间、团队空间,无需过审
|
||||||
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
|
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||||
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||||
//已改为编程式鉴权
|
//已改为编程式鉴权
|
||||||
|
@ -0,0 +1,262 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.disruptor.PictureEditEventProducer;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditActionEnum;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditMessageTypeEnum;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditResponseMessage;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.User;
|
||||||
|
import edu.whut.smilepicturebackend.service.UserService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑 WebSocket 处理器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class PictureEditHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
@Lazy
|
||||||
|
private PictureEditEventProducer pictureEditEventProducer;
|
||||||
|
|
||||||
|
// 每张图片的编辑状态,key: pictureId, value: 当前正在编辑的用户 ID
|
||||||
|
private final Map<Long, Long> pictureEditingUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 保存所有连接的会话,key: pictureId, value: 所有正在编辑这张图片的用户会话集合
|
||||||
|
private final Map<Long, Set<WebSocketSession>> pictureSessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实现连接建立成功后执行的方法,保存会话到集合中,并且给其他会话发送消息
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
|
super.afterConnectionEstablished(session);
|
||||||
|
// 保存会话到集合中
|
||||||
|
User user = (User) session.getAttributes().get("user");
|
||||||
|
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||||
|
pictureSessions.putIfAbsent(pictureId, ConcurrentHashMap.newKeySet());
|
||||||
|
pictureSessions.get(pictureId).add(session);
|
||||||
|
// 构造响应,发送加入编辑的消息通知
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
|
||||||
|
String message = String.format("用户 %s 加入编辑", user.getUserName());
|
||||||
|
pictureEditResponseMessage.setMessage(message);
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
// 广播给所有用户
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage); //自己也可以收到自己发的
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编写接收客户端消息的方法,根据消息类别执行不同的处理
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @param message
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||||
|
super.handleTextMessage(session, message);
|
||||||
|
// 获取消息内容,将 JSON 转换为 PictureEditRequestMessage
|
||||||
|
PictureEditRequestMessage pictureEditRequestMessage = JSONUtil.toBean(message.getPayload(), PictureEditRequestMessage.class);
|
||||||
|
// 从 Session 属性中获取到公共参数
|
||||||
|
User user = (User) session.getAttributes().get("user");
|
||||||
|
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||||
|
// 根据消息类型处理消息(生产消息到 Disruptor 环形队列中)
|
||||||
|
pictureEditEventProducer.publishEvent(pictureEditRequestMessage, session, user, pictureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入编辑状态
|
||||||
|
*
|
||||||
|
* @param pictureEditRequestMessage
|
||||||
|
* @param session
|
||||||
|
* @param user
|
||||||
|
* @param pictureId
|
||||||
|
*/
|
||||||
|
public void handleEnterEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||||
|
// 没有用户正在编辑该图片,才能进入编辑
|
||||||
|
if (!pictureEditingUsers.containsKey(pictureId)) {
|
||||||
|
// 设置用户正在编辑该图片
|
||||||
|
pictureEditingUsers.put(pictureId, user.getId());
|
||||||
|
// 构造响应,发送加入编辑的消息通知
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ENTER_EDIT.getValue());
|
||||||
|
String message = String.format("用户 %s 开始编辑图片", user.getUserName());
|
||||||
|
pictureEditResponseMessage.setMessage(message);
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
// 广播给所有用户
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理编辑操作
|
||||||
|
*
|
||||||
|
* @param pictureEditRequestMessage
|
||||||
|
* @param session
|
||||||
|
* @param user
|
||||||
|
* @param pictureId
|
||||||
|
*/
|
||||||
|
public void handleEditActionMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||||
|
// 正在编辑的用户
|
||||||
|
Long editingUserId = pictureEditingUsers.get(pictureId);
|
||||||
|
String editAction = pictureEditRequestMessage.getEditAction();
|
||||||
|
PictureEditActionEnum actionEnum = PictureEditActionEnum.getEnumByValue(editAction);
|
||||||
|
if (actionEnum == null) {
|
||||||
|
log.error("无效的编辑动作");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 确认是当前的编辑者
|
||||||
|
if (editingUserId != null && editingUserId.equals(user.getId())) {
|
||||||
|
// 构造响应,发送具体操作的通知
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EDIT_ACTION.getValue());
|
||||||
|
String message = String.format("%s 执行 %s", user.getUserName(), actionEnum.getText());
|
||||||
|
pictureEditResponseMessage.setMessage(message);
|
||||||
|
pictureEditResponseMessage.setEditAction(editAction);
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
// 广播给除了当前客户端之外的其他用户,否则会造成重复编辑
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户退出编辑操作时,移除当前用户的编辑状态,并且向其他客户端发送消息
|
||||||
|
*
|
||||||
|
* @param pictureEditRequestMessage
|
||||||
|
* @param session
|
||||||
|
* @param user
|
||||||
|
* @param pictureId
|
||||||
|
*/
|
||||||
|
public void handleExitEditMessage(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) throws IOException {
|
||||||
|
// 正在编辑的用户
|
||||||
|
Long editingUserId = pictureEditingUsers.get(pictureId);
|
||||||
|
// 确认是当前的编辑者
|
||||||
|
if (editingUserId != null && editingUserId.equals(user.getId())) {
|
||||||
|
// 移除用户正在编辑该图片
|
||||||
|
pictureEditingUsers.remove(pictureId);
|
||||||
|
// 构造响应,发送退出编辑的消息通知
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.EXIT_EDIT.getValue());
|
||||||
|
String message = String.format("用户 %s 退出编辑图片", user.getUserName());
|
||||||
|
pictureEditResponseMessage.setMessage(message);
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 连接关闭时,需要移除当前用户的编辑状态、并且从集合中删除当前会话,还可以给其他客户端发送消息通知
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @param status
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||||
|
super.afterConnectionClosed(session, status);
|
||||||
|
// 从 Session 属性中获取到公共参数
|
||||||
|
User user = (User) session.getAttributes().get("user");
|
||||||
|
Long pictureId = (Long) session.getAttributes().get("pictureId");
|
||||||
|
// 移除当前用户的编辑状态
|
||||||
|
handleExitEditMessage(null, session, user, pictureId);
|
||||||
|
// 删除会话
|
||||||
|
Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
|
||||||
|
if (sessionSet != null) {
|
||||||
|
sessionSet.remove(session);
|
||||||
|
if (sessionSet.isEmpty()) {
|
||||||
|
pictureSessions.remove(pictureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 通知其他用户,该用户已经离开编辑
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.INFO.getValue());
|
||||||
|
String message = String.format("用户 %s 离开编辑", user.getUserName());
|
||||||
|
pictureEditResponseMessage.setMessage(message);
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播给该图片的所有用户(支持排除掉某个 Session)
|
||||||
|
*
|
||||||
|
* @param pictureId
|
||||||
|
* @param pictureEditResponseMessage
|
||||||
|
* @param excludeSession
|
||||||
|
*/
|
||||||
|
private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage, WebSocketSession excludeSession) throws IOException {
|
||||||
|
Set<WebSocketSession> sessionSet = pictureSessions.get(pictureId);
|
||||||
|
if (CollUtil.isNotEmpty(sessionSet)) {
|
||||||
|
// 创建 ObjectMapper
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
// 配置序列化:将 Long 类型转为 String,解决丢失精度问题
|
||||||
|
SimpleModule module = new SimpleModule();
|
||||||
|
module.addSerializer(Long.class, ToStringSerializer.instance);
|
||||||
|
module.addSerializer(Long.TYPE, ToStringSerializer.instance); // 支持 long 基本类型
|
||||||
|
objectMapper.registerModule(module);
|
||||||
|
// 序列化为 JSON 字符串
|
||||||
|
String message = objectMapper.writeValueAsString(pictureEditResponseMessage);
|
||||||
|
TextMessage textMessage = new TextMessage(message);
|
||||||
|
for (WebSocketSession session : sessionSet) {
|
||||||
|
// 排除掉的 session 不发送 (比如自己发送的广播,自己不接收)
|
||||||
|
if (excludeSession != null && session.equals(excludeSession)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (session.isOpen()) {
|
||||||
|
session.sendMessage(textMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 广播给该图片的所有用户
|
||||||
|
*
|
||||||
|
* @param pictureId
|
||||||
|
* @param pictureEditResponseMessage
|
||||||
|
*/
|
||||||
|
private void broadcastToPicture(Long pictureId, PictureEditResponseMessage pictureEditResponseMessage) throws IOException {
|
||||||
|
broadcastToPicture(pictureId, pictureEditResponseMessage, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 配置(定义连接)
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocket
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
|
private final PictureEditHandler pictureEditHandler;
|
||||||
|
|
||||||
|
private final WsHandshakeInterceptor wsHandshakeInterceptor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
|
////当客户端在浏览器中执行new WebSocket("ws://<你的域名或 IP>:<端口>/ws/picture/edit?pictureId=123");就会由 Spring 把这个请求路由到你的 PictureEditHandler 实例。
|
||||||
|
registry.addHandler(pictureEditHandler, "/ws/picture/edit")
|
||||||
|
.addInterceptors(wsHandshakeInterceptor)
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import edu.whut.smilepicturebackend.manager.auth.SpaceUserAuthManager;
|
||||||
|
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.User;
|
||||||
|
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||||
|
import edu.whut.smilepicturebackend.service.PictureService;
|
||||||
|
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||||
|
import edu.whut.smilepicturebackend.service.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.http.server.ServletServerHttpRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 拦截器,建立连接前要先校验 /如果只是公开的广播通道,不必写拦截器。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WsHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
private final PictureService pictureService;
|
||||||
|
|
||||||
|
private final SpaceService spaceService;
|
||||||
|
|
||||||
|
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建立连接前要先校验
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @param response
|
||||||
|
* @param wsHandler
|
||||||
|
* @param attributes 给 WebSocketSession 会话设置属性
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||||
|
if (request instanceof ServletServerHttpRequest) {
|
||||||
|
HttpServletRequest httpServletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||||
|
// 从请求中获取参数
|
||||||
|
String pictureId = httpServletRequest.getParameter("pictureId");
|
||||||
|
if (StrUtil.isBlank(pictureId)) {
|
||||||
|
log.error("缺少图片参数,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 获取当前登录用户
|
||||||
|
User loginUser = userService.getLoginUser(httpServletRequest);
|
||||||
|
if (ObjUtil.isEmpty(loginUser)) {
|
||||||
|
log.error("用户未登录,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 校验用户是否有编辑当前图片的权限
|
||||||
|
Picture picture = pictureService.getById(pictureId);
|
||||||
|
if (ObjUtil.isEmpty(picture)) {
|
||||||
|
log.error("图片不存在,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Long spaceId = picture.getSpaceId();
|
||||||
|
Space space = null;
|
||||||
|
if (spaceId != null) {
|
||||||
|
space = spaceService.getById(spaceId);
|
||||||
|
if (ObjUtil.isEmpty(space)) {
|
||||||
|
log.error("图片所在空间不存在,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (space.getSpaceType() != SpaceTypeEnum.TEAM.getValue()) {
|
||||||
|
log.error("图片所在空间不是团队空间,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> permissionList = spaceUserAuthManager.getPermissionList(space, loginUser);
|
||||||
|
if (!permissionList.contains(SpaceUserPermissionConstant.PICTURE_EDIT)) {
|
||||||
|
log.error("用户没有编辑图片的权限,拒绝握手");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 如果握手成功,设置用户登录信息等属性到 WebSocket 会话的属性 Map中
|
||||||
|
attributes.put("user", loginUser);
|
||||||
|
attributes.put("userId", loginUser.getId());
|
||||||
|
attributes.put("pictureId", Long.valueOf(pictureId)); // 记得转换为 Long 类型
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||||
|
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.User;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑事件
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PictureEditEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息
|
||||||
|
*/
|
||||||
|
private PictureEditRequestMessage pictureEditRequestMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户的 session
|
||||||
|
*/
|
||||||
|
private WebSocketSession session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户
|
||||||
|
*/
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片 id
|
||||||
|
*/
|
||||||
|
private Long pictureId;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||||
|
|
||||||
|
import cn.hutool.core.thread.ThreadFactoryBuilder;
|
||||||
|
import com.lmax.disruptor.dsl.Disruptor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑事件 Disruptor 配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class PictureEditEventDisruptorConfig {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private PictureEditEventWorkHandler pictureEditEventWorkHandler;
|
||||||
|
|
||||||
|
@Bean("pictureEditEventDisruptor")
|
||||||
|
public Disruptor<PictureEditEvent> messageModelRingBuffer() {
|
||||||
|
// 定义 ringBuffer 的大小 ,小了可能数据来不及消费
|
||||||
|
int bufferSize = 1024 * 256;
|
||||||
|
// 创建 disruptor
|
||||||
|
Disruptor<PictureEditEvent> disruptor = new Disruptor<>(
|
||||||
|
PictureEditEvent::new,
|
||||||
|
bufferSize,
|
||||||
|
ThreadFactoryBuilder.create().setNamePrefix("pictureEditEventDisruptor").build()
|
||||||
|
);
|
||||||
|
// 设置消费者
|
||||||
|
disruptor.handleEventsWithWorkerPool(pictureEditEventWorkHandler);
|
||||||
|
// 启动 disruptor
|
||||||
|
disruptor.start();
|
||||||
|
return disruptor;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||||
|
|
||||||
|
import com.lmax.disruptor.RingBuffer;
|
||||||
|
import com.lmax.disruptor.dsl.Disruptor;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.User;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑事件(生产者)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class PictureEditEventProducer {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private Disruptor<PictureEditEvent> pictureEditEventDisruptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布事件
|
||||||
|
*
|
||||||
|
* @param pictureEditRequestMessage
|
||||||
|
* @param session
|
||||||
|
* @param user
|
||||||
|
* @param pictureId
|
||||||
|
*/
|
||||||
|
public void publishEvent(PictureEditRequestMessage pictureEditRequestMessage, WebSocketSession session, User user, Long pictureId) {
|
||||||
|
RingBuffer<PictureEditEvent> ringBuffer = pictureEditEventDisruptor.getRingBuffer();
|
||||||
|
// 获取到可以防止事件的位置
|
||||||
|
long next = ringBuffer.next();
|
||||||
|
PictureEditEvent pictureEditEvent = ringBuffer.get(next);
|
||||||
|
pictureEditEvent.setPictureEditRequestMessage(pictureEditRequestMessage);
|
||||||
|
pictureEditEvent.setSession(session);
|
||||||
|
pictureEditEvent.setUser(user);
|
||||||
|
pictureEditEvent.setPictureId(pictureId);
|
||||||
|
// 发布事件
|
||||||
|
ringBuffer.publish(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优雅停机
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
pictureEditEventDisruptor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.disruptor;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import com.lmax.disruptor.WorkHandler;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.PictureEditHandler;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditMessageTypeEnum;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditRequestMessage;
|
||||||
|
import edu.whut.smilepicturebackend.manager.websocket.model.PictureEditResponseMessage;
|
||||||
|
import edu.whut.smilepicturebackend.model.entity.User;
|
||||||
|
import edu.whut.smilepicturebackend.service.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑事件处理器(消费者)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PictureEditEventWorkHandler implements WorkHandler<PictureEditEvent> {
|
||||||
|
|
||||||
|
private final PictureEditHandler pictureEditHandler;
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(PictureEditEvent pictureEditEvent) throws Exception {
|
||||||
|
PictureEditRequestMessage pictureEditRequestMessage = pictureEditEvent.getPictureEditRequestMessage();
|
||||||
|
WebSocketSession session = pictureEditEvent.getSession();
|
||||||
|
User user = pictureEditEvent.getUser();
|
||||||
|
Long pictureId = pictureEditEvent.getPictureId();
|
||||||
|
// 获取到消息类别
|
||||||
|
String type = pictureEditRequestMessage.getType();
|
||||||
|
PictureEditMessageTypeEnum pictureEditMessageTypeEnum = PictureEditMessageTypeEnum.getEnumByValue(type);
|
||||||
|
// 根据消息类型处理消息
|
||||||
|
switch (pictureEditMessageTypeEnum) {
|
||||||
|
case ENTER_EDIT:
|
||||||
|
pictureEditHandler.handleEnterEditMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||||
|
break;
|
||||||
|
case EXIT_EDIT:
|
||||||
|
pictureEditHandler.handleExitEditMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||||
|
break;
|
||||||
|
case EDIT_ACTION:
|
||||||
|
pictureEditHandler.handleEditActionMessage(pictureEditRequestMessage, session, user, pictureId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// 其他消息类型,返回错误提示
|
||||||
|
PictureEditResponseMessage pictureEditResponseMessage = new PictureEditResponseMessage();
|
||||||
|
pictureEditResponseMessage.setType(PictureEditMessageTypeEnum.ERROR.getValue());
|
||||||
|
pictureEditResponseMessage.setMessage("消息类型错误");
|
||||||
|
pictureEditResponseMessage.setUser(userService.getUserVO(user));
|
||||||
|
session.sendMessage(new TextMessage(JSONUtil.toJsonStr(pictureEditResponseMessage)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑动作枚举
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum PictureEditActionEnum {
|
||||||
|
|
||||||
|
ZOOM_IN("放大操作", "ZOOM_IN"),
|
||||||
|
ZOOM_OUT("缩小操作", "ZOOM_OUT"),
|
||||||
|
ROTATE_LEFT("左旋操作", "ROTATE_LEFT"),
|
||||||
|
ROTATE_RIGHT("右旋操作", "ROTATE_RIGHT");
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
PictureEditActionEnum(String text, String value) {
|
||||||
|
this.text = text;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 value 获取枚举
|
||||||
|
*/
|
||||||
|
public static PictureEditActionEnum getEnumByValue(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (PictureEditActionEnum actionEnum : PictureEditActionEnum.values()) {
|
||||||
|
if (actionEnum.value.equals(value)) {
|
||||||
|
return actionEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑消息类型枚举
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum PictureEditMessageTypeEnum {
|
||||||
|
|
||||||
|
INFO("发送通知", "INFO"),
|
||||||
|
ERROR("发送错误", "ERROR"),
|
||||||
|
ENTER_EDIT("进入编辑状态", "ENTER_EDIT"),
|
||||||
|
EXIT_EDIT("退出编辑状态", "EXIT_EDIT"),
|
||||||
|
EDIT_ACTION("执行编辑操作", "EDIT_ACTION");
|
||||||
|
|
||||||
|
private final String text;
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
PictureEditMessageTypeEnum(String text, String value) {
|
||||||
|
this.text = text;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 value 获取枚举
|
||||||
|
*/
|
||||||
|
public static PictureEditMessageTypeEnum getEnumByValue(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (PictureEditMessageTypeEnum typeEnum : PictureEditMessageTypeEnum.values()) {
|
||||||
|
if (typeEnum.value.equals(value)) {
|
||||||
|
return typeEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑请求消息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PictureEditRequestMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型,例如 "ENTER_EDIT", "EXIT_EDIT", "EDIT_ACTION"
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 光有EDIT_ACTION不够,还要有执行的编辑动作(放大、缩小)
|
||||||
|
*/
|
||||||
|
private String editAction;
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package edu.whut.smilepicturebackend.manager.websocket.model;
|
||||||
|
|
||||||
|
import edu.whut.smilepicturebackend.model.vo.UserVO;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片编辑响应消息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PictureEditResponseMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型,例如 "INFO", "ERROR", "ENTER_EDIT", "EXIT_EDIT", "EDIT_ACTION"
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信息
|
||||||
|
*/
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行的编辑动作
|
||||||
|
*/
|
||||||
|
private String editAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
private UserVO user;
|
||||||
|
}
|
@ -89,6 +89,10 @@ public class PictureVO implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空间 id
|
||||||
|
*/
|
||||||
|
private Long spaceId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
|
@ -119,9 +119,9 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||||
// 改为使用统一的权限校验
|
// 改为使用统一的权限校验
|
||||||
// 校验是否有空间的权限,仅空间管理员才能上传
|
// 校验是否有空间的权限,仅空间管理员才能上传
|
||||||
if (!loginUser.getId().equals(space.getUserId())) {
|
// if (!loginUser.getId().equals(space.getUserId())) {
|
||||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
||||||
}
|
// }
|
||||||
// 校验额度
|
// 校验额度
|
||||||
if (space.getTotalCount() >= space.getMaxCount()) {
|
if (space.getTotalCount() >= space.getMaxCount()) {
|
||||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间条数不足");
|
throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间条数不足");
|
||||||
@ -138,10 +138,11 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
if (pictureId != null) {
|
if (pictureId != null) {
|
||||||
oldPicture = this.getById(pictureId);
|
oldPicture = this.getById(pictureId);
|
||||||
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
|
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
|
||||||
|
// 改为使用统一的权限校验
|
||||||
// 仅本人或管理员可编辑图片
|
// 仅本人或管理员可编辑图片
|
||||||
if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
|
// if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
|
||||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||||
}
|
// }
|
||||||
// 校验空间是否一致
|
// 校验空间是否一致
|
||||||
// 没传 spaceId,则复用原有图片的 spaceId(这样也兼容了公共图库)
|
// 没传 spaceId,则复用原有图片的 spaceId(这样也兼容了公共图库)
|
||||||
if (spaceId == null) {
|
if (spaceId == null) {
|
||||||
@ -189,7 +190,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
|||||||
picture.setUserId(loginUser.getId());
|
picture.setUserId(loginUser.getId());
|
||||||
picture.setSpaceId(spaceId);
|
picture.setSpaceId(spaceId);
|
||||||
// 转换为标准颜色
|
// 转换为标准颜色
|
||||||
log.info("颜色"+uploadPictureResult.getPicColor());
|
// log.info("颜色"+uploadPictureResult.getPicColor());
|
||||||
picture.setPicColor(ColorTransformUtils.getStandardColor(uploadPictureResult.getPicColor()));
|
picture.setPicColor(ColorTransformUtils.getStandardColor(uploadPictureResult.getPicColor()));
|
||||||
// 补充审核参数
|
// 补充审核参数
|
||||||
this.fillReviewParams(picture, loginUser);
|
this.fillReviewParams(picture, loginUser);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user