fixed web-socket and add rewards on post

This commit is contained in:
SlimusMinus
2026-08-12 00:41:31 +03:00
parent f3ba7e6512
commit 13ed934c60
12 changed files with 107 additions and 52 deletions

View File

@@ -1,8 +1,9 @@
package com.krylov.refound.config; package com.krylov.refound.config;
import com.krylov.refound.util.WsHandshakeInterceptor; import com.krylov.refound.util.WsStompInterceptor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@@ -13,19 +14,23 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
@RequiredArgsConstructor @RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final WsHandshakeInterceptor wsHandshakeInterceptor; private final WsStompInterceptor wsStompInterceptor;
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); // сервер → клиент registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app"); // клиент → сервер registry.setApplicationDestinationPrefixes("/app");
} }
@Override @Override
public void registerStompEndpoints(StompEndpointRegistry registry) { public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws") registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*") .setAllowedOriginPatterns("*")
.addInterceptors(wsHandshakeInterceptor)
.withSockJS(); .withSockJS();
} }
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(wsStompInterceptor);
}
} }

View File

@@ -2,6 +2,7 @@ package com.krylov.refound.controller;
import com.krylov.refound.dto.SendMessageRequest; import com.krylov.refound.dto.SendMessageRequest;
import com.krylov.refound.service.ChatService; import com.krylov.refound.service.ChatService;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -14,25 +15,38 @@ import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
public class ChatWebSocketController { public class ChatWebSocketController {
private final ChatService chatService; private final ChatService chatService;
private final SimpMessagingTemplate messagingTemplate; private final SimpMessagingTemplate messagingTemplate;
// Клиент шлёт сюда: /app/chat.send // Клиент шлёт сюда: /app/chat.send
@MessageMapping("/chat.send") @MessageMapping("/chat.send")
public void sendMessage(SendMessageRequest request, SimpMessageHeaderAccessor headerAccessor) { public void sendMessage(SendMessageRequest request, SimpMessageHeaderAccessor headerAccessor) {
Long senderId = (Long) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("userId"); Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();
if (senderId == null) {
log.warn("sendMessage: senderId is null, session attrs missing userId"); if (sessionAttributes == null) {
return; // или отправить ошибку клиенту log.warn("sendMessage: sessionAttributes is null");
return;
} }
Object userIdObject = sessionAttributes.get("userId");
if (userIdObject == null) {
log.warn("sendMessage: userId missing in session attributes");
return;
}
Long senderId = ((Number) userIdObject).longValue();
if (request.getContent() == null || request.getContent().isBlank()) { if (request.getContent() == null || request.getContent().isBlank()) {
return; return;
} }
try { try {
chatService.sendMessage(senderId, request.getChatId(), request.getContent()); chatService.sendMessage(senderId, request.getChatId(), request.getContent());
} catch (SecurityException | IllegalArgumentException e) { } catch (SecurityException | IllegalArgumentException e) {
messagingTemplate.convertAndSendToUser(senderId.toString(), "/queue/errors", e.getMessage()); messagingTemplate.convertAndSendToUser(senderId.toString(), "/queue/errors", e.getMessage()
);
} }
// ChatService сам отправит сообщение всем подписчикам через messagingTemplate
} }
} }

View File

@@ -54,10 +54,10 @@ public class PostController {
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public PostResponse update(@PathVariable Long id, @RequestParam String type, @RequestParam String title, @RequestParam(required = false) String address, public PostResponse update(@PathVariable Long id, @RequestParam String type, @RequestParam String title, @RequestParam(required = false) String address,
@RequestParam(required = false) Double latitude, @RequestParam(required = false) Double longitude, @RequestParam(required = false) String description, @RequestParam(required = false) Double latitude, @RequestParam(required = false) Double longitude, @RequestParam(required = false) String description,
@RequestParam(required = false) String category, @RequestParam(required = false) String phone, @RequestParam(required = false) String category, @RequestParam(required = false) String phone, @RequestParam(required = false) Boolean reward,
@RequestParam(value = "images", required = false) List<MultipartFile> images, @RequestParam(value = "existingImages", required = false) String existingImagesJson) { @RequestParam(value = "images", required = false) List<MultipartFile> images, @RequestParam(value = "existingImages", required = false) String existingImagesJson) {
log.info("update post {}", id); log.info("update post {}", id);
return service.update(id, type, title, address, latitude, longitude, description, category, phone, images, existingImagesJson); return service.update(id, type, title, address, latitude, longitude, description, category, phone, reward, images, existingImagesJson);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")

View File

@@ -19,4 +19,5 @@ public class PostRequest {
private Double longitude; private Double longitude;
private String address; private String address;
private String rulesAccepted; private String rulesAccepted;
private Boolean reward;
} }

View File

@@ -27,4 +27,5 @@ public class PostResponse {
private Long likeCount; private Long likeCount;
private Role userRole; private Role userRole;
private String rulesAccepted; private String rulesAccepted;
private Boolean reward;
} }

View File

@@ -42,6 +42,7 @@ public class Post {
private LocalDateTime createdAt; private LocalDateTime createdAt;
private String phone; private String phone;
private String rulesAccepted; private String rulesAccepted;
private Boolean isReward;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private PostType type; private PostType type;

View File

@@ -64,6 +64,7 @@ public class PostService {
post.setCreatedAt(LocalDateTime.now()); post.setCreatedAt(LocalDateTime.now());
post.setStatus(PostStatus.MODERATION); post.setStatus(PostStatus.MODERATION);
post.setUser(currentUser); post.setUser(currentUser);
post.setIsReward(request.getReward());
if (request.getLatitude() == null || request.getLongitude() == null) { if (request.getLatitude() == null || request.getLongitude() == null) {
post.setCity(request.getAddress()); post.setCity(request.getAddress());
} else { } else {
@@ -134,7 +135,7 @@ public class PostService {
*/ */
@Transactional @Transactional
public PostResponse update(Long id, String type, String title, String address, Double latitude, public PostResponse update(Long id, String type, String title, String address, Double latitude,
Double longitude, String description, String category, String phone, List<MultipartFile> images, String existingImagesJson) { Double longitude, String description, String category, String phone, Boolean reward, List<MultipartFile> images, String existingImagesJson) {
PostRequest request = new PostRequest(); PostRequest request = new PostRequest();
request.setType(PostType.from(type)); request.setType(PostType.from(type));
@@ -145,6 +146,7 @@ public class PostService {
request.setLatitude(latitude); request.setLatitude(latitude);
request.setLongitude(longitude); request.setLongitude(longitude);
request.setAddress(address); request.setAddress(address);
request.setReward(reward);
List<String> existingUrls = parseExistingImages(existingImagesJson); List<String> existingUrls = parseExistingImages(existingImagesJson);
@@ -163,6 +165,7 @@ public class PostService {
} }
post.setPhone(request.getPhone()); post.setPhone(request.getPhone());
post.setType(request.getType()); post.setType(request.getType());
post.setIsReward(request.getReward());
if (request.getLatitude() != null && request.getLongitude() != null) { if (request.getLatitude() != null && request.getLongitude() != null) {
post.setLatitude(request.getLatitude()); post.setLatitude(request.getLatitude());
@@ -361,6 +364,7 @@ public class PostService {
PostResponse response = mapper.toResponse(post); PostResponse response = mapper.toResponse(post);
response.setLikeCount(likeCounts.getOrDefault(post.getId(), 0L)); response.setLikeCount(likeCounts.getOrDefault(post.getId(), 0L));
response.setFavorite(userFavoriteIds.contains(post.getId())); response.setFavorite(userFavoriteIds.contains(post.getId()));
response.setReward(post.getIsReward());
if (includeUserRole) { if (includeUserRole) {
response.setUserRole(post.getUser().getRole()); response.setUserRole(post.getUser().getRole());
} }

View File

@@ -1,37 +0,0 @@
package com.krylov.refound.util;
import com.krylov.refound.security.JwtService;
import com.krylov.refound.service.UserService;
import java.util.Map;
import lombok.RequiredArgsConstructor;
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;
@Component
@RequiredArgsConstructor
public class WsHandshakeInterceptor implements HandshakeInterceptor {
private final JwtService jwtService;
private final UserService userService;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest servletRequest) {
String token = servletRequest.getServletRequest().getParameter("token");
if (token != null && jwtService.isTokenValid(token)) {
String login = jwtService.extractLogin(token);
Long userId = userService.getUserIdByLogin(login);
attributes.put("userId", userId);
return true;
}
}
return false; // отклонить подключение без валидного токена
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}

View File

@@ -0,0 +1,53 @@
package com.krylov.refound.util;
import com.krylov.refound.security.JwtService;
import com.krylov.refound.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class WsStompInterceptor implements ChannelInterceptor {
private final JwtService jwtService;
private final UserService userService;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authorization = accessor.getFirstNativeHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new IllegalArgumentException("Missing Authorization header");
}
String token = authorization.substring(7);
if (!jwtService.isTokenValid(token)) {
throw new IllegalArgumentException("Invalid JWT token");
}
String login = jwtService.extractLogin(token);
Long userId = userService.getUserIdByLogin(login);
if (userId == null) {
throw new IllegalArgumentException("User not found");
}
accessor.getSessionAttributes().put("userId", userId);
}
return message;
}
}

View File

@@ -54,4 +54,4 @@ jwt:
app: app:
cors: cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:https://refound.example.com,http://localhost:3000} allowed-origins: http://localhost:5173,http://localhost:3000

View File

@@ -0,0 +1,12 @@
databaseChangeLog:
- changeSet:
id: 014-add-reward-posts
author: alexandr krylov
changes:
- addColumn:
tableName: posts
columns:
- column:
name: is_reward
type: boolean
defaultValueBoolean: false

View File

@@ -18,3 +18,4 @@ databaseChangeLog:
- include: { file: db/changelog/add/013-add-rules_accepted-posts.yaml} - include: { file: db/changelog/add/013-add-rules_accepted-posts.yaml}
- include: { file: db/changelog/create/009-create-statistic.yaml } - include: { file: db/changelog/create/009-create-statistic.yaml }
- include: { file: db/changelog/alter/011-alter-posts-district-length.yaml} - include: { file: db/changelog/alter/011-alter-posts-district-length.yaml}
- include: { file: db/changelog/add/014-add-reward-posts.yaml }