4.29 多人协同编辑图片 websocket Disruptor

This commit is contained in:
zhangsan 2025-04-29 18:14:55 +08:00
parent 6a5fb45cc2
commit 528422fed6
16 changed files with 747 additions and 11 deletions

11
pom.xml
View File

@ -102,6 +102,17 @@
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.0</version>
</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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>

View File

@ -13,8 +13,14 @@ public class MainController {
* 健康检查
*/
@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");
}
}

View File

@ -175,7 +175,7 @@ public class PictureController {
// 查询数据库
Picture picture = pictureService.getById(id);
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();
Space space = null;
@ -186,6 +186,8 @@ public class PictureController {
//User loginUser = userService.getLoginUser(request);
//已经改为使用sa-token鉴权
//pictureService.checkPictureAuth(loginUser, picture);
space = spaceService.getById(spaceId);
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
}
// 获取权限列表
User loginUser = userService.getLoginUser(request);
@ -228,7 +230,7 @@ public class PictureController {
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
pictureQueryRequest.setNullSpaceId(true);
} else {
// 私有空间
// 私有空间团队空间无需过审
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
//已改为编程式鉴权

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -89,6 +89,10 @@ public class PictureVO implements Serializable {
*/
private Long userId;
/**
* 空间 id
*/
private Long spaceId;
/**
* 创建时间

View File

@ -119,9 +119,9 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
// 改为使用统一的权限校验
// 校验是否有空间的权限仅空间管理员才能上传
if (!loginUser.getId().equals(space.getUserId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
}
// if (!loginUser.getId().equals(space.getUserId())) {
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
// }
// 校验额度
if (space.getTotalCount() >= space.getMaxCount()) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "空间条数不足");
@ -138,10 +138,11 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
if (pictureId != null) {
oldPicture = this.getById(pictureId);
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
// 改为使用统一的权限校验
// 仅本人或管理员可编辑图片
if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
// }
// 校验空间是否一致
// 没传 spaceId则复用原有图片的 spaceId这样也兼容了公共图库
if (spaceId == null) {
@ -189,7 +190,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
picture.setUserId(loginUser.getId());
picture.setSpaceId(spaceId);
// 转换为标准颜色
log.info("颜色"+uploadPictureResult.getPicColor());
// log.info("颜色"+uploadPictureResult.getPicColor());
picture.setPicColor(ColorTransformUtils.getStandardColor(uploadPictureResult.getPicColor()));
// 补充审核参数
this.fillReviewParams(picture, loginUser);