fixed controllers
This commit is contained in:
@@ -10,7 +10,6 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
import org.springframework.web.reactive.function.client.WebClientRequestException;
|
||||||
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -19,7 +18,7 @@ public class AiHttpClient {
|
|||||||
private final WebClient aiWebClient;
|
private final WebClient aiWebClient;
|
||||||
|
|
||||||
public <T, R> R post(String uri, T request, Class<R> responseType) {
|
public <T, R> R post(String uri, T request, Class<R> responseType) {
|
||||||
log.info("AI POST {}", uri);
|
log.info("AI TEXT POST {}", uri);
|
||||||
try {
|
try {
|
||||||
R response = aiWebClient.post()
|
R response = aiWebClient.post()
|
||||||
.uri(uri)
|
.uri(uri)
|
||||||
|
|||||||
@@ -21,18 +21,14 @@ public class AiMultipartHttpClient {
|
|||||||
private final WebClient aiWebClient;
|
private final WebClient aiWebClient;
|
||||||
|
|
||||||
public <R> R post(String uri, MultipartFile file, Class<R> responseType) {
|
public <R> R post(String uri, MultipartFile file, Class<R> responseType) {
|
||||||
log.info("AI POST {}", uri);
|
log.info("AI IMAGE POST {}", uri);
|
||||||
try {
|
try {
|
||||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||||
builder.part(
|
builder.part("file", new ByteArrayResource(file.getBytes()) {
|
||||||
"file",
|
|
||||||
new ByteArrayResource(file.getBytes()) {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return file.getOriginalFilename();
|
return file.getOriginalFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,8 +52,10 @@ public class AiMultipartHttpClient {
|
|||||||
log.info("AI image response received");
|
log.info("AI image response received");
|
||||||
return response;
|
return response;
|
||||||
} catch (WebClientRequestException ex) {
|
} catch (WebClientRequestException ex) {
|
||||||
|
log.error("WebClientRequestException in AiMultipartHttpClient {}", ex.getMessage(), ex);
|
||||||
throw new AiUnavailableException(ex);
|
throw new AiUnavailableException(ex);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
log.error("Exception in AiMultipartHttpClient {}", ex.getMessage(), ex);
|
||||||
throw new RuntimeException("Cannot send image to AI", ex);
|
throw new RuntimeException("Cannot send image to AI", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
package com.krylov.refound.ai.client;
|
package com.krylov.refound.ai.client;
|
||||||
|
|
||||||
import com.krylov.refound.ai.dto.ImageModerationResponse;
|
import com.krylov.refound.ai.dto.ImageModerationResponse;
|
||||||
|
import com.krylov.refound.ai.dto.ModerationResponse;
|
||||||
import com.krylov.refound.ai.exception.AiServerException;
|
import com.krylov.refound.ai.exception.AiServerException;
|
||||||
import com.krylov.refound.ai.exception.AiUnavailableException;
|
import com.krylov.refound.ai.exception.AiUnavailableException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.retry.annotation.Backoff;
|
import org.springframework.retry.annotation.Backoff;
|
||||||
|
import org.springframework.retry.annotation.Recover;
|
||||||
import org.springframework.retry.annotation.Retryable;
|
import org.springframework.retry.annotation.Retryable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ImageModerationClient {
|
public class ImageModerationClient {
|
||||||
private final AiMultipartHttpClient client;
|
private final AiMultipartHttpClient client;
|
||||||
|
|
||||||
@Retryable(
|
@Retryable(
|
||||||
retryFor = {
|
retryFor = {AiUnavailableException.class, AiServerException.class},
|
||||||
AiUnavailableException.class,
|
|
||||||
AiServerException.class
|
|
||||||
},
|
|
||||||
maxAttempts = 3,
|
maxAttempts = 3,
|
||||||
backoff = @Backoff(
|
backoff = @Backoff(delay = 500, multiplier = 2)
|
||||||
delay = 500,
|
|
||||||
multiplier = 2
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
public ImageModerationResponse moderate(
|
public ImageModerationResponse moderate(MultipartFile file) {
|
||||||
MultipartFile file
|
log.info("Calling AI ImageModeration service");
|
||||||
) {
|
return client.post("/api/v1/moderation/image", file, ImageModerationResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
return client.post(
|
@Recover
|
||||||
"/api/v1/moderation/image",
|
public ModerationResponse recover(AiUnavailableException e, String text) {
|
||||||
file,
|
log.error("AI ImageModeration unavailable after retries", e);
|
||||||
ImageModerationResponse.class
|
|
||||||
);
|
|
||||||
|
|
||||||
|
return ModerationResponse.builder()
|
||||||
|
.approved(false)
|
||||||
|
.reason("AI_UNAVAILABLE")
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,29 +19,18 @@ public class TextModerationClient {
|
|||||||
private final AiHttpClient client;
|
private final AiHttpClient client;
|
||||||
|
|
||||||
@Retryable(
|
@Retryable(
|
||||||
retryFor = {
|
retryFor = {AiUnavailableException.class, AiServerException.class},
|
||||||
AiUnavailableException.class,
|
|
||||||
AiServerException.class
|
|
||||||
},
|
|
||||||
maxAttempts = 3,
|
maxAttempts = 3,
|
||||||
backoff = @Backoff(
|
backoff = @Backoff(delay = 500, multiplier = 2)
|
||||||
delay = 500,
|
|
||||||
multiplier = 2
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
public ModerationResponse moderate(String text) {
|
public ModerationResponse moderate(String text) {
|
||||||
log.info("Calling AI service");
|
log.info("Calling AI TextModeration service");
|
||||||
return client.post(
|
return client.post("/api/v1/moderation/text", new TextModerationRequest(text), ModerationResponse.class);
|
||||||
"/api/v1/moderation/text",
|
|
||||||
new TextModerationRequest(text),
|
|
||||||
ModerationResponse.class
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Recover
|
@Recover
|
||||||
public ModerationResponse recover(AiUnavailableException e, String text) {
|
public ModerationResponse recover(AiUnavailableException e, String text) {
|
||||||
log.error("AI unavailable after retries", e);
|
log.error("AI TextModeration unavailable after retries", e);
|
||||||
|
|
||||||
return ModerationResponse.builder()
|
return ModerationResponse.builder()
|
||||||
.approved(false)
|
.approved(false)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package com.krylov.refound.ai.config;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ConfigurationProperties(prefix = "ai")
|
@ConfigurationProperties(prefix = "ai")
|
||||||
|
@ToString
|
||||||
public class AiProperties {
|
public class AiProperties {
|
||||||
/**
|
/**
|
||||||
* http://localhost:8000
|
* http://localhost:8000
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.krylov.refound.ai.config;
|
package com.krylov.refound.ai.config;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import reactor.netty.http.client.HttpClient;
|
import reactor.netty.http.client.HttpClient;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -8,26 +9,23 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(AiProperties.class)
|
@EnableConfigurationProperties(AiProperties.class)
|
||||||
public class WebClientConfig {
|
public class WebClientConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public WebClient aiWebClient(AiProperties properties) {
|
public WebClient aiWebClient(AiProperties properties) {
|
||||||
|
log.info("aiWebClient get for properties {}", properties);
|
||||||
HttpClient httpClient =
|
HttpClient httpClient =
|
||||||
HttpClient.create()
|
HttpClient.create()
|
||||||
.responseTimeout(
|
.responseTimeout(
|
||||||
Duration.ofSeconds(
|
Duration.ofSeconds(properties.getTimeout())
|
||||||
properties.getTimeout()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return WebClient.builder()
|
return WebClient.builder()
|
||||||
.baseUrl(properties.getUrl())
|
.baseUrl(properties.getUrl())
|
||||||
.clientConnector(
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
new ReactorClientHttpConnector(httpClient)
|
|
||||||
)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package com.krylov.refound.ai.pipeline;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentModerationPipeline {
|
public class ContentModerationPipeline {
|
||||||
@@ -11,6 +13,7 @@ public class ContentModerationPipeline {
|
|||||||
private final List<ModerationProcessor> processors;
|
private final List<ModerationProcessor> processors;
|
||||||
|
|
||||||
public void execute(ModerationContext context) {
|
public void execute(ModerationContext context) {
|
||||||
|
log.info("execute ContentModerationPipeline");
|
||||||
for (ModerationProcessor processor : processors) {
|
for (ModerationProcessor processor : processors) {
|
||||||
processor.process(context);
|
processor.process(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package com.krylov.refound.ai.pipeline;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.ToString;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@ToString
|
||||||
@Builder
|
@Builder
|
||||||
public class ModerationContext {
|
public class ModerationContext {
|
||||||
private String title;
|
private String title;
|
||||||
|
|||||||
@@ -18,32 +18,22 @@ public class TextModerationProcessor implements ModerationProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(ModerationContext context) {
|
public void process(ModerationContext context) {
|
||||||
|
log.info("process TextModerationProcessor for context {}", context);
|
||||||
String text = buildText(
|
String text = buildText(
|
||||||
context.getTitle(),
|
context.getTitle(),
|
||||||
context.getDescription()
|
context.getDescription()
|
||||||
);
|
);
|
||||||
|
|
||||||
ModerationResponse response = client.moderate(text);
|
ModerationResponse response = client.moderate(text);
|
||||||
|
log.info("AI moderation result approved={}, score={}, reason={}", response.getApproved(), response.getScore(), response.getReason());
|
||||||
log.info(
|
|
||||||
"AI moderation result approved={}, score={}, reason={}",
|
|
||||||
response.getApproved(),
|
|
||||||
response.getScore(),
|
|
||||||
response.getReason()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.getApproved()) {
|
if (!response.getApproved()) {
|
||||||
throw new ContentBlockedException(
|
throw new ContentBlockedException(response.getReason(), response.getDetectedLabels());
|
||||||
response.getReason(),
|
|
||||||
response.getDetectedLabels()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildText(String title, String description) {
|
private String buildText(String title, String description) {
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
@@ -51,8 +41,7 @@ public class TextModerationProcessor implements ModerationProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
builder.append("\n")
|
builder.append("\n").append(description);
|
||||||
.append(description);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.krylov.refound.config;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
|
||||||
@Value("${file.upload-dir}")
|
|
||||||
private String uploadDir;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
|
||||||
registry
|
|
||||||
.addResourceHandler("/files/**")
|
|
||||||
.addResourceLocations("file:" + uploadDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
|
||||||
registry.addMapping("/**")
|
|
||||||
.allowedOriginPatterns("*") // 🔥 важно
|
|
||||||
.allowedMethods("*")
|
|
||||||
.allowedHeaders("*")
|
|
||||||
.allowCredentials(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.krylov.refound.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
|
||||||
registry
|
|
||||||
.addResourceHandler("/uploads/**")
|
|
||||||
.addResourceLocations("file:uploads/"); // папка uploads/ в корне проекта
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,12 @@ import com.krylov.refound.dto.AuthRequest;
|
|||||||
import com.krylov.refound.dto.AuthResponse;
|
import com.krylov.refound.dto.AuthResponse;
|
||||||
import com.krylov.refound.dto.CheckLoginDto;
|
import com.krylov.refound.dto.CheckLoginDto;
|
||||||
import com.krylov.refound.dto.RegisterRequest;
|
import com.krylov.refound.dto.RegisterRequest;
|
||||||
import com.krylov.refound.dto.UserDto;
|
import com.krylov.refound.dto.UserRegisterDto;
|
||||||
import com.krylov.refound.entity.User;
|
import com.krylov.refound.service.AuthService;
|
||||||
import com.krylov.refound.repository.UserRepository;
|
|
||||||
import com.krylov.refound.security.JwtService;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -24,73 +21,28 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final AuthService authService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
private final JwtService jwtService;
|
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
|
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
|
||||||
|
log.info("register user {}", request);
|
||||||
// Проверка что логин не занят
|
UserRegisterDto userRegisterDto = authService.registerUser(request);
|
||||||
if (userRepository.findByLogin(request.getLogin()).isPresent()) {
|
return ResponseEntity.ok(
|
||||||
return ResponseEntity
|
new AuthResponse(userRegisterDto.getToken(), userRegisterDto.getUserDto()));
|
||||||
.badRequest()
|
|
||||||
.body(Map.of("message", "Логин уже занят"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создание пользователя
|
|
||||||
User user = new User();
|
|
||||||
user.setName(request.getName());
|
|
||||||
user.setLogin(request.getLogin());
|
|
||||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
|
||||||
|
|
||||||
userRepository.save(user);
|
|
||||||
|
|
||||||
String token = jwtService.generateToken(user.getLogin());
|
|
||||||
|
|
||||||
UserDto userDto = new UserDto(
|
|
||||||
user.getId(),
|
|
||||||
user.getName(),
|
|
||||||
user.getLastName(),
|
|
||||||
user.getLogin(),
|
|
||||||
user.getEmail(),
|
|
||||||
user.getPhone(),
|
|
||||||
user.getAvatarUrl(),
|
|
||||||
user.getRole()
|
|
||||||
);
|
|
||||||
return ResponseEntity.ok(new AuthResponse(token, userDto));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<?> login(@RequestBody AuthRequest request) {
|
public ResponseEntity<?> login(@RequestBody AuthRequest request) {
|
||||||
|
log.info("user {} logging", request.getLogin());
|
||||||
User user = userRepository.findByLogin(request.getLogin())
|
UserRegisterDto userRegisterDto = authService.loginUser(request);
|
||||||
.orElse(null);
|
return ResponseEntity.ok(
|
||||||
|
new AuthResponse(userRegisterDto.getToken(), userRegisterDto.getUserDto()));
|
||||||
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
|
||||||
return ResponseEntity
|
|
||||||
.badRequest()
|
|
||||||
.body(Map.of("message", "Неверный логин или пароль"));
|
|
||||||
}
|
|
||||||
|
|
||||||
String token = jwtService.generateToken(user.getLogin());
|
|
||||||
|
|
||||||
UserDto userDto = new UserDto(
|
|
||||||
user.getId(),
|
|
||||||
user.getName(),
|
|
||||||
user.getLastName(),
|
|
||||||
user.getLogin(),
|
|
||||||
user.getEmail(),
|
|
||||||
user.getPhone(),
|
|
||||||
user.getAvatarUrl(),
|
|
||||||
user.getRole()
|
|
||||||
);
|
|
||||||
return ResponseEntity.ok(new AuthResponse(token, userDto));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/check-login")
|
@PostMapping("/check-login")
|
||||||
public ResponseEntity<?> checkLogin(@RequestBody CheckLoginDto request) {
|
public ResponseEntity<?> checkLogin(@RequestBody CheckLoginDto request) {
|
||||||
boolean exists = userRepository.existsByLogin(request.getLogin());
|
log.info("checkLogin for {}", request.getLogin());
|
||||||
|
boolean exists = authService.existsByLogin(request);
|
||||||
return ResponseEntity.ok(Map.of("exists", exists));
|
return ResponseEntity.ok(Map.of("exists", exists));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ import com.krylov.refound.dto.*;
|
|||||||
import com.krylov.refound.service.ChatService;
|
import com.krylov.refound.service.ChatService;
|
||||||
import com.krylov.refound.service.UserService;
|
import com.krylov.refound.service.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/v1/chats")
|
@RequestMapping("/api/v1/chats")
|
||||||
@@ -20,40 +26,46 @@ public class ChatController {
|
|||||||
// Создать чат с пользователем или получить существующий
|
// Создать чат с пользователем или получить существующий
|
||||||
@PostMapping("/with/{otherUserId}")
|
@PostMapping("/with/{otherUserId}")
|
||||||
public ChatDto createOrGetChat(@PathVariable Long otherUserId, @RequestParam(required = false) Long postId, Authentication auth) {
|
public ChatDto createOrGetChat(@PathVariable Long otherUserId, @RequestParam(required = false) Long postId, Authentication auth) {
|
||||||
Long currentUserId = extractUserId(auth); // см. ниже как достать id
|
Long currentUserId = extractUserId(auth);
|
||||||
|
log.info("createOrGetChat currentUserId {}, otherUserId {}, postId {}", currentUserId, otherUserId, postId);
|
||||||
return chatService.getOrCreateChat(currentUserId, otherUserId, postId);
|
return chatService.getOrCreateChat(currentUserId, otherUserId, postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Список всех чатов текущего пользователя
|
// Список всех чатов текущего пользователя
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<ChatDto> getMyChats(Authentication auth) {
|
public List<ChatDto> getMyChats(Authentication auth) {
|
||||||
return chatService.getUserChats(extractUserId(auth));
|
Long currentUserId = extractUserId(auth);
|
||||||
|
log.info("getMyChats for user {}", currentUserId);
|
||||||
|
return chatService.getUserChats(currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// История сообщений конкретного чата
|
// История сообщений конкретного чата, с пагинацией
|
||||||
@GetMapping("/{chatId}/messages")
|
@GetMapping("/{chatId}/messages")
|
||||||
public List<MessageDto> getMessages(@PathVariable Long chatId) {
|
public Page<MessageDto> getMessages(@PathVariable Long chatId, @PageableDefault(size = 30, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable, Authentication auth) {
|
||||||
return chatService.getMessages(chatId);
|
Long currentUserId = extractUserId(auth);
|
||||||
}
|
log.info("getMessages for chatId {} page {} by user {}", chatId, pageable.getPageNumber(), currentUserId);
|
||||||
|
return chatService.getMessages(chatId, currentUserId, pageable);
|
||||||
private Long extractUserId(Authentication auth) {
|
|
||||||
// Подставь свою логику получения userId из JWT/Authentication
|
|
||||||
// Например если в токене login, а не id - подтяни через UserService
|
|
||||||
String login = auth.getName();
|
|
||||||
return userService.getUserIdByLogin(login);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{chatId}")
|
@DeleteMapping("/{chatId}")
|
||||||
public void deleteChat(@PathVariable Long chatId, Authentication auth) {
|
public void deleteChat(@PathVariable Long chatId, Authentication auth) {
|
||||||
Long currentUserId = extractUserId(auth);
|
Long currentUserId = extractUserId(auth);
|
||||||
chatService.deleteChat(chatId);
|
log.info("user {} deleteChat {}", currentUserId, chatId);
|
||||||
|
chatService.deleteChat(chatId, currentUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/v1/chats/{chatId}/read")
|
@PostMapping("/{chatId}/read")
|
||||||
public void markAsRead(@PathVariable Long chatId) {
|
public void markAsRead(@PathVariable Long chatId, Authentication auth) {
|
||||||
Long userId = (Long) SecurityContextHolder.getContext()
|
Long currentUserId = extractUserId(auth);
|
||||||
.getAuthentication().getPrincipal(); // или твой способ
|
log.info("markAsRead chatId {} by user {}", chatId, currentUserId);
|
||||||
chatService.markAsRead(chatId, userId);
|
chatService.markAsRead(chatId, currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long extractUserId(Authentication auth) {
|
||||||
|
// Подставь свою логику получения userId из JWT/Authentication
|
||||||
|
String login = auth.getName();
|
||||||
|
log.debug("extractUserId by login {}", login);
|
||||||
|
return userService.getUserIdByLogin(login);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,37 @@ 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.Objects;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChatWebSocketController {
|
public class ChatWebSocketController {
|
||||||
private final ChatService chatService;
|
private final ChatService chatService;
|
||||||
|
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) headerAccessor.getSessionAttributes().get("userId");
|
Long senderId = (Long) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("userId");
|
||||||
chatService.sendMessage(senderId, request.getChatId(), request.getContent());
|
if (senderId == null) {
|
||||||
|
log.warn("sendMessage: senderId is null, session attrs missing userId");
|
||||||
|
return; // или отправить ошибку клиенту
|
||||||
|
}
|
||||||
|
if (request.getContent() == null || request.getContent().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
chatService.sendMessage(senderId, request.getChatId(), request.getContent());
|
||||||
|
} catch (SecurityException | IllegalArgumentException e) {
|
||||||
|
messagingTemplate.convertAndSendToUser(senderId.toString(), "/queue/errors", e.getMessage());
|
||||||
|
}
|
||||||
// ChatService сам отправит сообщение всем подписчикам через messagingTemplate
|
// ChatService сам отправит сообщение всем подписчикам через messagingTemplate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
package com.krylov.refound.controller;
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
import com.krylov.refound.dto.PostResponse;
|
import com.krylov.refound.dto.PostResponse;
|
||||||
import com.krylov.refound.mapper.PostMapper;
|
|
||||||
import com.krylov.refound.service.FavoriteService;
|
import com.krylov.refound.service.FavoriteService;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/favorites")
|
@RequestMapping("/api/v1/favorites")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class FavoriteController {
|
public class FavoriteController {
|
||||||
|
|
||||||
private final FavoriteService service;
|
private final FavoriteService service;
|
||||||
private final PostMapper mapper;
|
|
||||||
|
|
||||||
@PostMapping("/{postId}/toggle")
|
@PostMapping("/{postId}/toggle")
|
||||||
public void toggle(@PathVariable Long postId) {
|
public void toggle(@PathVariable Long postId) {
|
||||||
|
log.info("toggle post {}", postId);
|
||||||
service.toggle(postId);
|
service.toggle(postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<PostResponse> getAll() {
|
public List<PostResponse> getAll() {
|
||||||
return service.getFavorites()
|
log.info("get all likes");
|
||||||
.stream()
|
return service.getFavorites();
|
||||||
.map(mapper::toResponse)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ package com.krylov.refound.controller;
|
|||||||
|
|
||||||
import com.krylov.refound.service.FileStorageService;
|
import com.krylov.refound.service.FileStorageService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.util.UriUtils;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/files")
|
@RequestMapping("/api/v1/files")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -19,26 +22,21 @@ public class FileStorageController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
|
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
|
||||||
|
log.info("uploadFile");
|
||||||
String objectName = fileStorageService.uploadFile(file);
|
String objectName = fileStorageService.uploadFile(file);
|
||||||
return ResponseEntity.ok(objectName); // фронт сохранит objectName как ключ картинки
|
return ResponseEntity.ok(objectName); // фронт сохранит objectName как ключ картинки
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{objectName}")
|
@GetMapping("/{*objectName}")
|
||||||
public ResponseEntity<byte[]> downloadFile(@PathVariable String objectName) {
|
public ResponseEntity<byte[]> downloadFile(@PathVariable String objectName) {
|
||||||
FileStorageService.DownloadedFile downloadedFile = fileStorageService.downloadFile(objectName);
|
// objectName придёт как "/avatars/cf80aebe-...jpg" — обрежь ведущий слэш
|
||||||
|
String key = objectName.startsWith("/") ? objectName.substring(1) : objectName;
|
||||||
String encodedFilename = UriUtils.encode(downloadedFile.originalFilename(), StandardCharsets.UTF_8);
|
return fileStorageService.downloadFile(key);
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename);
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.headers(headers)
|
|
||||||
.contentType(MediaType.parseMediaType(downloadedFile.contentType()))
|
|
||||||
.body(downloadedFile.content());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{objectName}")
|
@DeleteMapping("/{objectName}")
|
||||||
public ResponseEntity<Void> deleteFile(@PathVariable String objectName) {
|
public ResponseEntity<Void> deleteFile(@PathVariable String objectName) {
|
||||||
|
log.info("deleteFile {}", objectName);
|
||||||
fileStorageService.deleteFile(objectName);
|
fileStorageService.deleteFile(objectName);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
package com.krylov.refound.controller;
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.krylov.refound.dto.PostRequest;
|
import com.krylov.refound.dto.PostRequest;
|
||||||
import com.krylov.refound.dto.PostResponse;
|
import com.krylov.refound.dto.PostResponse;
|
||||||
import com.krylov.refound.dto.StatusUpdateRequest;
|
import com.krylov.refound.dto.StatusUpdateRequest;
|
||||||
import com.krylov.refound.enums.PostType;
|
|
||||||
import com.krylov.refound.service.PostService;
|
import com.krylov.refound.service.PostService;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.data.web.PageableDefault;
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -27,78 +22,53 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/posts")
|
@RequestMapping("/api/v1/posts")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
|
||||||
public class PostController {
|
public class PostController {
|
||||||
|
|
||||||
private final PostService service;
|
private final PostService service;
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public PostRequest create(@ModelAttribute PostRequest request, @RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
public PostRequest create(@ModelAttribute PostRequest request, @RequestParam(value = "images", required = false) List<MultipartFile> images) {
|
||||||
log.info(request.toString());
|
log.info("create post {}", request.toString());
|
||||||
return service.create(request, images);
|
return service.create(request, images);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public Page<PostResponse> getFeed(
|
public Page<PostResponse> getFeed(@RequestParam(required = false) String search, @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
|
||||||
@RequestParam(required = false) String search,
|
log.info("get all feed");
|
||||||
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
|
|
||||||
return service.getFeed(search, pageable);
|
return service.getFeed(search, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public List<PostResponse> getById(@PathVariable Long id) {
|
public List<PostResponse> getById(@PathVariable Long id) {
|
||||||
|
log.info("get feed by id {}", id);
|
||||||
return service.getById(id);
|
return service.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
public PostResponse update(
|
public PostResponse update(@PathVariable Long id, @RequestParam String type, @RequestParam String title, @RequestParam(required = false) String address,
|
||||||
@PathVariable Long id,
|
@RequestParam(required = false) Double latitude, @RequestParam(required = false) Double longitude, @RequestParam(required = false) String description,
|
||||||
@RequestParam String type,
|
@RequestParam(required = false) String category, @RequestParam(required = false) String phone,
|
||||||
@RequestParam String title,
|
@RequestParam(value = "images", required = false) List<MultipartFile> images, @RequestParam(value = "existingImages", required = false) String existingImagesJson) {
|
||||||
@RequestParam(required = false) String address,
|
log.info("update post {}", id);
|
||||||
@RequestParam(required = false) Double latitude,
|
return service.update(id, type, title, address, latitude, longitude, description, category, phone, images, existingImagesJson);
|
||||||
@RequestParam(required = false) Double longitude,
|
|
||||||
@RequestParam(required = false) String description,
|
|
||||||
@RequestParam(required = false) String category,
|
|
||||||
@RequestParam(required = false) String phone,
|
|
||||||
@RequestParam(value = "images", required = false) List<MultipartFile> images,
|
|
||||||
@RequestParam(value = "existingImages", required = false) String existingImagesJson) {
|
|
||||||
PostRequest request = new PostRequest();
|
|
||||||
request.setType(PostType.from(type));
|
|
||||||
request.setTitle(title);
|
|
||||||
request.setDescription(description);
|
|
||||||
request.setCategory(category);
|
|
||||||
request.setPhone(phone);
|
|
||||||
request.setLatitude(latitude);
|
|
||||||
request.setLongitude(longitude);
|
|
||||||
request.setAddress(address);
|
|
||||||
List<String> existingUrls;
|
|
||||||
try {
|
|
||||||
existingUrls = existingImagesJson != null ? Arrays.asList(
|
|
||||||
objectMapper.readValue(existingImagesJson, String[].class)) : List.of();
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.update(id, request, images, existingUrls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public void delete(@PathVariable Long id) {
|
public void delete(@PathVariable Long id) {
|
||||||
|
log.info("delete feed by id {}", id);
|
||||||
service.delete(id);
|
service.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping("/{id}/status")
|
@PatchMapping("/{id}/status")
|
||||||
public PostResponse updateStatus(@PathVariable Long id,
|
public PostResponse updateStatus(@PathVariable Long id, @RequestBody StatusUpdateRequest request) {
|
||||||
@RequestBody StatusUpdateRequest request) {
|
log.info("update feed {} on status {}", id, request.getStatus());
|
||||||
return service.updateStatus(id, request.getStatus());
|
return service.updateStatus(id, request.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,25 +15,24 @@ import org.springframework.web.bind.annotation.RequestBody;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/reviews")
|
@RequestMapping("/api/v1/reviews")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
|
||||||
public class ReviewsController {
|
public class ReviewsController {
|
||||||
|
|
||||||
private final ReviewService reviewService;
|
private final ReviewService reviewService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<ReviewResponseDto> getAllReviews() {
|
public List<ReviewResponseDto> getAllReviews() {
|
||||||
List<ReviewResponseDto> allReviews = reviewService.getAllReviews();
|
log.info("get all reviews");
|
||||||
log.info("Got all reviews: {}", allReviews);
|
return reviewService.getAllReviews();
|
||||||
return allReviews;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ReviewResponseDto createReview(@Valid @RequestBody ReviewRequestDto request, Authentication authentication) {
|
public ReviewResponseDto createReview(@Valid @RequestBody ReviewRequestDto request, Authentication authentication) {
|
||||||
// Взять имя пользователя из authentication
|
log.info("user {} create review {}", authentication.getName(), request);
|
||||||
return reviewService.createReview(request, authentication.getName());
|
return reviewService.createReview(request, authentication.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package com.krylov.refound.controller;
|
|||||||
import com.krylov.refound.entity.Statistics;
|
import com.krylov.refound.entity.Statistics;
|
||||||
import com.krylov.refound.service.StatisticsService;
|
import com.krylov.refound.service.StatisticsService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/v1/statistics")
|
@RequestMapping("/api/v1/statistics")
|
||||||
@@ -15,6 +17,7 @@ public class StatisticsController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public Statistics get() {
|
public Statistics get() {
|
||||||
|
log.info("get all statistics");
|
||||||
return statisticsService.getStatistics();
|
return statisticsService.getStatistics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class UserController {
|
|||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<UserResponseDto> updateUser(@PathVariable Long id, @ModelAttribute UserUpdateDto userUpdateDto) {
|
public ResponseEntity<UserResponseDto> updateUser(@PathVariable Long id, @ModelAttribute UserUpdateDto userUpdateDto) {
|
||||||
log.info("updateUser, id={}", id);
|
log.info("updateUser, id={}", id);
|
||||||
UserResponseDto userResponseDto = userService.updateUser(id, userUpdateDto);
|
UserResponseDto userResponseDto = userService.updateUser(id, userUpdateDto);
|
||||||
return ResponseEntity.ok(userResponseDto);
|
return ResponseEntity.ok(userResponseDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/main/java/com/krylov/refound/dto/UserRegisterDto.java
Normal file
13
src/main/java/com/krylov/refound/dto/UserRegisterDto.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserRegisterDto {
|
||||||
|
private final UserDto userDto;
|
||||||
|
private final String token;
|
||||||
|
}
|
||||||
@@ -16,6 +16,17 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(InvalidCredentialsException.class)
|
||||||
|
public ResponseEntity<?> handleInvalidCredentials(InvalidCredentialsException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(LoginAlreadyExistsException.class)
|
||||||
|
public ResponseEntity<?> handleLoginAlreadyExists(LoginAlreadyExistsException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(ApiException.class)
|
@ExceptionHandler(ApiException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleApiException(ApiException ex, HttpServletRequest request) {
|
public ResponseEntity<ErrorResponse> handleApiException(ApiException ex, HttpServletRequest request) {
|
||||||
return buildResponse(ex.getStatus(), ex.getErrorCode(), ex.getMessage(), request);
|
return buildResponse(ex.getStatus(), ex.getErrorCode(), ex.getMessage(), request);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.krylov.refound.exception;
|
||||||
|
|
||||||
|
public class InvalidCredentialsException extends RuntimeException {
|
||||||
|
|
||||||
|
public InvalidCredentialsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.krylov.refound.exception;
|
||||||
|
|
||||||
|
public class LoginAlreadyExistsException extends RuntimeException {
|
||||||
|
|
||||||
|
public LoginAlreadyExistsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.krylov.refound.repository;
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
import com.krylov.refound.dto.DialogResponse;
|
|
||||||
import com.krylov.refound.entity.Message;
|
import com.krylov.refound.entity.Message;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
@@ -10,10 +12,14 @@ import org.springframework.data.repository.query.Param;
|
|||||||
|
|
||||||
public interface MessageRepository extends JpaRepository<Message, Long> {
|
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||||
|
|
||||||
List<Message> findByChatIdOrderByCreatedAtAsc(Long chatId);
|
Page<Message> findByChatIdOrderByCreatedAtDesc(Long chatId, Pageable pageable);
|
||||||
long countByChatIdAndIsReadFalseAndSenderIdNot(Long chatId, Long currentUserId);
|
|
||||||
|
Optional<Message> findFirstByChatIdOrderByCreatedAtDesc(Long chatId);
|
||||||
|
|
||||||
|
long countByChatIdAndIsReadFalseAndSenderIdNot(Long chatId, Long userId);
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query("UPDATE Message m SET m.isRead = true WHERE m.chatId = :chatId AND m.senderId != :userId")
|
@Query("update Message m set m.isRead = true where m.chatId = :chatId and m.senderId != :userId")
|
||||||
int markAsReadByChatIdAndRecipient(@Param("chatId") Long chatId, @Param("userId") Long userId);
|
void markAsReadByChatIdAndRecipient(@Param("chatId") Long chatId, @Param("userId") Long userId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/main/java/com/krylov/refound/service/AuthService.java
Normal file
77
src/main/java/com/krylov/refound/service/AuthService.java
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.AuthRequest;
|
||||||
|
import com.krylov.refound.dto.CheckLoginDto;
|
||||||
|
import com.krylov.refound.dto.RegisterRequest;
|
||||||
|
import com.krylov.refound.dto.UserDto;
|
||||||
|
import com.krylov.refound.dto.UserRegisterDto;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.exception.InvalidCredentialsException;
|
||||||
|
import com.krylov.refound.exception.LoginAlreadyExistsException;
|
||||||
|
import com.krylov.refound.repository.UserRepository;
|
||||||
|
import com.krylov.refound.security.JwtService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
public UserRegisterDto registerUser(RegisterRequest request){
|
||||||
|
if (userRepository.findByLogin(request.getLogin()).isPresent()) {
|
||||||
|
log.warn("registration failed: login {} already taken", request.getLogin());
|
||||||
|
throw new LoginAlreadyExistsException("Логин уже занят");
|
||||||
|
}
|
||||||
|
User user = new User();
|
||||||
|
user.setName(request.getName());
|
||||||
|
user.setLogin(request.getLogin());
|
||||||
|
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||||
|
|
||||||
|
userRepository.save(user);
|
||||||
|
|
||||||
|
String token = jwtService.generateToken(user.getLogin());
|
||||||
|
|
||||||
|
UserDto userDto = getUserDto(user);
|
||||||
|
|
||||||
|
return new UserRegisterDto(userDto, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRegisterDto loginUser(AuthRequest request){
|
||||||
|
User user = userRepository.findByLogin(request.getLogin()).orElse(null);
|
||||||
|
|
||||||
|
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||||
|
log.warn("login for user {} or password is incorrect", request.getLogin());
|
||||||
|
throw new InvalidCredentialsException("Неверный логин или пароль");
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = jwtService.generateToken(user.getLogin());
|
||||||
|
UserDto userDto = getUserDto(user);
|
||||||
|
|
||||||
|
return new UserRegisterDto(userDto, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsByLogin(CheckLoginDto request){
|
||||||
|
return userRepository.existsByLogin(request.getLogin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserDto getUserDto(User user) {
|
||||||
|
return new UserDto(
|
||||||
|
user.getId(),
|
||||||
|
user.getName(),
|
||||||
|
user.getLastName(),
|
||||||
|
user.getLogin(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getPhone(),
|
||||||
|
user.getAvatarUrl(),
|
||||||
|
user.getRole()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.krylov.refound.service;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class AvatarService {
|
|
||||||
private final Path uploadDir = Paths.get("uploads/avatars");
|
|
||||||
|
|
||||||
public String save(MultipartFile file) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(uploadDir);
|
|
||||||
String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
|
||||||
Path target = uploadDir.resolve(filename);
|
|
||||||
file.transferTo(target);
|
|
||||||
return "/uploads/avatars/" + filename; // или полный URL, если есть статик-сервер/CDN
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Failed to save avatar", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,17 +11,22 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ChatService {
|
public class ChatService {
|
||||||
|
|
||||||
private final ChatRepository chatRepository;
|
private final ChatRepository chatRepository;
|
||||||
private final MessageRepository messageRepository;
|
private final MessageRepository messageRepository;
|
||||||
private final SimpMessagingTemplate messagingTemplate;
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
private final UserRepository userRepository; // твой существующий репозиторий юзеров
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
// Создать чат с пользователем (или вернуть существующий)
|
// Создать чат с пользователем (или вернуть существующий)
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -46,28 +51,28 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Все чаты пользователя
|
// Все чаты пользователя
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public List<ChatDto> getUserChats(Long userId) {
|
public List<ChatDto> getUserChats(Long userId) {
|
||||||
return chatRepository.findAllByUserId(userId).stream()
|
return chatRepository.findAllByUserId(userId).stream()
|
||||||
.map(c -> toDto(c, userId))
|
.map(c -> toDto(c, userId))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// История сообщений в чате
|
// История сообщений в чате, с пагинацией и проверкой доступа
|
||||||
public List<MessageDto> getMessages(Long chatId) {
|
@Transactional(readOnly = true)
|
||||||
return messageRepository.findByChatIdOrderByCreatedAtAsc(chatId).stream()
|
public Page<MessageDto> getMessages(Long chatId, Long userId, Pageable pageable) {
|
||||||
.map(this::toMessageDto)
|
Chat chat = getChatOrThrow(chatId);
|
||||||
.collect(Collectors.toList());
|
checkParticipant(chat, userId);
|
||||||
|
|
||||||
|
return messageRepository.findByChatIdOrderByCreatedAtDesc(chatId, pageable)
|
||||||
|
.map(this::toMessageDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправка сообщения (вызывается из WebSocket контроллера)
|
// Отправка сообщения (вызывается из WebSocket контроллера)
|
||||||
@Transactional
|
@Transactional
|
||||||
public MessageDto sendMessage(Long senderId, Long chatId, String content) {
|
public MessageDto sendMessage(Long senderId, Long chatId, String content) {
|
||||||
Chat chat = chatRepository.findById(chatId)
|
Chat chat = getChatOrThrow(chatId);
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
checkParticipant(chat, senderId);
|
||||||
|
|
||||||
if (!chat.getUserOneId().equals(senderId) && !chat.getUserTwoId().equals(senderId)) {
|
|
||||||
throw new SecurityException("Вы не участник этого чата");
|
|
||||||
}
|
|
||||||
|
|
||||||
Message message = new Message();
|
Message message = new Message();
|
||||||
message.setChatId(chatId);
|
message.setChatId(chatId);
|
||||||
@@ -95,6 +100,33 @@ public class ChatService {
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteChat(Long chatId, Long userId) {
|
||||||
|
Chat chat = getChatOrThrow(chatId);
|
||||||
|
checkParticipant(chat, userId);
|
||||||
|
chatRepository.deleteById(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void markAsRead(Long chatId, Long userId) {
|
||||||
|
Chat chat = getChatOrThrow(chatId);
|
||||||
|
checkParticipant(chat, userId);
|
||||||
|
messageRepository.markAsReadByChatIdAndRecipient(chatId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- вспомогательные методы ----------
|
||||||
|
|
||||||
|
private Chat getChatOrThrow(Long chatId) {
|
||||||
|
return chatRepository.findById(chatId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Чат не найден"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkParticipant(Chat chat, Long userId) {
|
||||||
|
if (!chat.getUserOneId().equals(userId) && !chat.getUserTwoId().equals(userId)) {
|
||||||
|
throw new SecurityException("Вы не участник этого чата");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ChatDto toDto(Chat chat, Long currentUserId) {
|
private ChatDto toDto(Chat chat, Long currentUserId) {
|
||||||
Long otherUserId = chat.getUserOneId().equals(currentUserId)
|
Long otherUserId = chat.getUserOneId().equals(currentUserId)
|
||||||
? chat.getUserTwoId() : chat.getUserOneId();
|
? chat.getUserTwoId() : chat.getUserOneId();
|
||||||
@@ -104,18 +136,17 @@ public class ChatService {
|
|||||||
dto.setOtherUserId(otherUserId);
|
dto.setOtherUserId(otherUserId);
|
||||||
dto.setPostId(chat.getPostId());
|
dto.setPostId(chat.getPostId());
|
||||||
|
|
||||||
// Подтягиваем имя/аватар собеседника из твоего UserRepository
|
|
||||||
userRepository.findById(otherUserId).ifPresent(user -> {
|
userRepository.findById(otherUserId).ifPresent(user -> {
|
||||||
dto.setOtherUserName(user.getName() + " " + user.getLastName());
|
dto.setOtherUserName(user.getName() + " " + user.getLastName());
|
||||||
dto.setOtherUserAvatar(user.getAvatarUrl());
|
dto.setOtherUserAvatar(user.getAvatarUrl());
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Message> messages = messageRepository.findByChatIdOrderByCreatedAtAsc(chat.getId());
|
// без N+1: берём только последнее сообщение, а не всю историю
|
||||||
if (!messages.isEmpty()) {
|
messageRepository.findFirstByChatIdOrderByCreatedAtDesc(chat.getId())
|
||||||
Message last = messages.getLast();
|
.ifPresent(last -> {
|
||||||
dto.setLastMessage(last.getContent());
|
dto.setLastMessage(last.getContent());
|
||||||
dto.setLastMessageAt(last.getCreatedAt());
|
dto.setLastMessageAt(last.getCreatedAt());
|
||||||
}
|
});
|
||||||
|
|
||||||
dto.setUnreadCount(
|
dto.setUnreadCount(
|
||||||
messageRepository.countByChatIdAndIsReadFalseAndSenderIdNot(chat.getId(), currentUserId)
|
messageRepository.countByChatIdAndIsReadFalseAndSenderIdNot(chat.getId(), currentUserId)
|
||||||
@@ -134,13 +165,4 @@ public class ChatService {
|
|||||||
dto.setCreatedAt(m.getCreatedAt());
|
dto.setCreatedAt(m.getCreatedAt());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteChat(Long chatId) {
|
|
||||||
chatRepository.deleteById(chatId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public void markAsRead(Long chatId, Long userId) {
|
|
||||||
messageRepository.markAsReadByChatIdAndRecipient(chatId, userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.PostResponse;
|
||||||
import com.krylov.refound.entity.Favorite;
|
import com.krylov.refound.entity.Favorite;
|
||||||
import com.krylov.refound.entity.Post;
|
import com.krylov.refound.entity.Post;
|
||||||
import com.krylov.refound.entity.User;
|
import com.krylov.refound.entity.User;
|
||||||
import com.krylov.refound.enums.ErrorCode;
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
import com.krylov.refound.exception.ApiException;
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import com.krylov.refound.mapper.PostMapper;
|
||||||
import com.krylov.refound.repository.FavoriteRepository;
|
import com.krylov.refound.repository.FavoriteRepository;
|
||||||
import com.krylov.refound.repository.PostRepository;
|
import com.krylov.refound.repository.PostRepository;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -18,44 +21,19 @@ public class FavoriteService {
|
|||||||
private final FavoriteRepository repository;
|
private final FavoriteRepository repository;
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final PostMapper mapper;
|
||||||
|
|
||||||
public void addToFavorites(Long postId) {
|
@Transactional(readOnly = true)
|
||||||
|
public List<PostResponse> getFavorites() {
|
||||||
User user = userService.getCurrentUser();
|
User user = userService.getCurrentUser();
|
||||||
|
|
||||||
Post post = postRepository.findById(postId)
|
|
||||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
|
||||||
|
|
||||||
if (repository.existsByUserAndPost(user, post)) {
|
|
||||||
return; // уже есть
|
|
||||||
}
|
|
||||||
|
|
||||||
Favorite favorite = new Favorite();
|
|
||||||
favorite.setUser(user);
|
|
||||||
favorite.setPost(post);
|
|
||||||
|
|
||||||
repository.save(favorite);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeFromFavorites(Long postId) {
|
|
||||||
|
|
||||||
User user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
Post post = postRepository.findById(postId)
|
|
||||||
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
|
||||||
|
|
||||||
repository.deleteByUserAndPost(user, post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Post> getFavorites() {
|
|
||||||
User user = userService.getCurrentUser();
|
|
||||||
|
|
||||||
return repository.findByUser(user)
|
return repository.findByUser(user)
|
||||||
.stream()
|
.stream()
|
||||||
.map(Favorite::getPost)
|
.map(Favorite::getPost)
|
||||||
|
.map(mapper::toResponse)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public void toggle(Long postId) {
|
public void toggle(Long postId) {
|
||||||
|
|
||||||
User user = userService.getCurrentUser();
|
User user = userService.getCurrentUser();
|
||||||
|
|||||||
@@ -1,140 +1,203 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
import com.krylov.refound.config.MinioProperties;
|
import com.krylov.refound.config.MinioProperties;
|
||||||
import lombok.RequiredArgsConstructor;
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import com.krylov.refound.exception.ApiException;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
import software.amazon.awssdk.core.ResponseBytes;
|
|
||||||
import software.amazon.awssdk.core.sync.RequestBody;
|
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
|
||||||
import software.amazon.awssdk.services.s3.model.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import org.springframework.web.util.UriUtils;
|
||||||
|
import software.amazon.awssdk.core.ResponseBytes;
|
||||||
|
import software.amazon.awssdk.core.sync.RequestBody;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||||
|
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
||||||
|
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
|
||||||
|
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
||||||
|
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class FileStorageService {
|
public class FileStorageService {
|
||||||
|
|
||||||
|
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||||
|
private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
|
||||||
|
"image/jpeg", "image/png", "image/webp"
|
||||||
|
);
|
||||||
|
|
||||||
private final S3Client s3Client;
|
private final S3Client s3Client;
|
||||||
private final MinioProperties properties;
|
private final MinioProperties properties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO для передачи данных скачанного файла.
|
* DTO для передачи данных скачанного файла.
|
||||||
* @param originalFilename Оригинальное имя файла.
|
|
||||||
* @param contentType MIME-тип файла.
|
|
||||||
* @param content Содержимое файла в виде массива байт.
|
|
||||||
*/
|
*/
|
||||||
public record DownloadedFile(
|
private record DownloadedFile(
|
||||||
String originalFilename,
|
String originalFilename,
|
||||||
String contentType,
|
String contentType,
|
||||||
byte[] content
|
byte[] content
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Загружает файл в MinIO, сохраняя оригинальное имя в метаданных,
|
* Загружает файл в MinIO в корень бакета.
|
||||||
* и возвращает его уникальное имя (включая расширение).
|
|
||||||
*
|
|
||||||
* @param file Загружаемый файл.
|
|
||||||
* @return Сгенерированное уникальное имя файла.
|
|
||||||
*/
|
*/
|
||||||
public String uploadFile(MultipartFile file) {
|
public String uploadFile(MultipartFile file) {
|
||||||
|
return uploadFile(file, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает файл в MinIO в указанную "папку" (префикс ключа),
|
||||||
|
* например "avatars" — сохранится как avatars/<uuid>.ext
|
||||||
|
*/
|
||||||
|
public String uploadFile(MultipartFile file, String folder) {
|
||||||
|
validateFile(file);
|
||||||
|
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String extension = extractExtension(originalFilename);
|
||||||
|
String prefix = (folder != null && !folder.isBlank())
|
||||||
|
? folder.replaceAll("/+$", "") + "/"
|
||||||
|
: "";
|
||||||
|
String objectName = prefix + UUID.randomUUID() + extension;
|
||||||
|
|
||||||
|
log.info("Загрузка файла '{}'. Новое имя: {}", originalFilename, objectName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (file.isEmpty()) {
|
|
||||||
log.error("Попытка загрузить пустой файл.");
|
|
||||||
throw new RuntimeException("Файл не может быть пустым.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
String extension = "";
|
|
||||||
if (originalFilename != null && originalFilename.contains(".")) {
|
|
||||||
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
||||||
}
|
|
||||||
|
|
||||||
String objectName = UUID.randomUUID().toString() + extension;
|
|
||||||
log.info("Загрузка файла '{}'. Новое имя: {}", originalFilename, objectName);
|
|
||||||
|
|
||||||
Map<String, String> metadata = new HashMap<>();
|
Map<String, String> metadata = new HashMap<>();
|
||||||
metadata.put("original-filename", URLEncoder.encode(originalFilename, StandardCharsets.UTF_8));
|
metadata.put("original-filename", URLEncoder.encode(originalFilename, StandardCharsets.UTF_8));
|
||||||
|
|
||||||
PutObjectRequest request = PutObjectRequest.builder()
|
PutObjectRequest request = PutObjectRequest.builder()
|
||||||
.bucket(properties.bucket())
|
.bucket(properties.bucket())
|
||||||
.key(objectName)
|
.key(objectName)
|
||||||
.contentType(file.getContentType())
|
.contentType(file.getContentType())
|
||||||
.metadata(metadata)
|
.metadata(metadata)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
|
s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
|
||||||
log.info("Файл '{}' успешно загружен.", objectName);
|
log.info("Файл '{}' успешно загружен.", objectName);
|
||||||
return objectName;
|
return objectName;
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Ошибка при чтении файла для загрузки: {}", e.getMessage());
|
log.error("Ошибка при чтении файла для загрузки: {}", e.getMessage(), e);
|
||||||
throw new RuntimeException("Ошибка при обработке файла.", e);
|
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при обработке файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Не удалось загрузить файл: {}", e.getMessage());
|
log.error("Не удалось загрузить файл: {}", e.getMessage(), e);
|
||||||
throw new RuntimeException("Ошибка при загрузке файла.", e);
|
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при загрузке файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачивает файл из MinIO.
|
* Скачивает файл из MinIO и собирает готовый HTTP-ответ с нужными заголовками.
|
||||||
*
|
* Вся логика формирования Content-Disposition/Content-Type инкапсулирована здесь,
|
||||||
* @param objectName Уникальное имя файла в хранилище.
|
* контроллеру остаётся только вернуть результат.
|
||||||
* @return DTO с содержимым файла и его метаданными.
|
|
||||||
*/
|
*/
|
||||||
public DownloadedFile downloadFile(String objectName) {
|
public ResponseEntity<byte[]> downloadFile(String objectName) {
|
||||||
try {
|
DownloadedFile file = fetchFile(objectName);
|
||||||
// 1. Получаем метаданные объекта, чтобы извлечь оригинальное имя
|
|
||||||
HeadObjectRequest headRequest = HeadObjectRequest.builder()
|
|
||||||
.bucket(properties.bucket())
|
|
||||||
.key(objectName)
|
|
||||||
.build();
|
|
||||||
HeadObjectResponse headResponse = s3Client.headObject(headRequest);
|
|
||||||
// Декодируем имя файла из метаданных
|
|
||||||
String originalFilename = URLDecoder.decode(headResponse.metadata().get("original-filename"), StandardCharsets.UTF_8);
|
|
||||||
String contentType = headResponse.contentType();
|
|
||||||
|
|
||||||
// 2. Получаем сам объект (его содержимое)
|
String encodedFilename = UriUtils.encode(file.originalFilename(), StandardCharsets.UTF_8);
|
||||||
GetObjectRequest getRequest = GetObjectRequest.builder()
|
String disposition = file.contentType().startsWith("image/") ? "inline" : "attachment";
|
||||||
.bucket(properties.bucket())
|
|
||||||
.key(objectName)
|
|
||||||
.build();
|
|
||||||
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getRequest);
|
|
||||||
|
|
||||||
log.info("Файл '{}' (оригинальное имя: '{}') успешно скачан.", objectName, originalFilename);
|
HttpHeaders headers = new HttpHeaders();
|
||||||
return new DownloadedFile(originalFilename, contentType, responseBytes.asByteArray());
|
headers.add(HttpHeaders.CONTENT_DISPOSITION, disposition + "; filename*=UTF-8''" + encodedFilename);
|
||||||
|
|
||||||
} catch (Exception e) {
|
return ResponseEntity.ok()
|
||||||
log.error("Ошибка при скачивании файла '{}': {}", objectName, e.getMessage());
|
.headers(headers)
|
||||||
throw new RuntimeException("Ошибка при скачивании файла.", e);
|
.contentType(MediaType.parseMediaType(file.contentType()))
|
||||||
}
|
.body(file.content());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удаляет файл из MinIO по его имени.
|
* Удаляет файл из MinIO по его имени.
|
||||||
*
|
|
||||||
* @param objectName Имя файла для удаления.
|
|
||||||
*/
|
*/
|
||||||
public void deleteFile(String objectName) {
|
public void deleteFile(String objectName) {
|
||||||
try {
|
try {
|
||||||
log.info("Запрос на удаление файла: {}", objectName);
|
log.info("Запрос на удаление файла: {}", objectName);
|
||||||
DeleteObjectRequest request = DeleteObjectRequest.builder()
|
DeleteObjectRequest request = DeleteObjectRequest.builder()
|
||||||
.bucket(properties.bucket())
|
.bucket(properties.bucket())
|
||||||
.key(objectName)
|
.key(objectName)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
s3Client.deleteObject(request);
|
s3Client.deleteObject(request);
|
||||||
log.info("Файл '{}' успешно удален.", objectName);
|
log.info("Файл '{}' успешно удален.", objectName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Ошибка при удалении файла '{}': {}", objectName, e.getMessage());
|
log.error("Ошибка при удалении файла '{}': {}", objectName, e.getMessage(), e);
|
||||||
throw new RuntimeException("Ошибка при удалении файла.", e);
|
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при удалении файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- вспомогательные методы ----------
|
||||||
|
|
||||||
|
private DownloadedFile fetchFile(String objectName) {
|
||||||
|
try {
|
||||||
|
HeadObjectRequest headRequest = HeadObjectRequest.builder()
|
||||||
|
.bucket(properties.bucket())
|
||||||
|
.key(objectName)
|
||||||
|
.build();
|
||||||
|
HeadObjectResponse headResponse = s3Client.headObject(headRequest);
|
||||||
|
|
||||||
|
String originalFilename = URLDecoder.decode(
|
||||||
|
headResponse.metadata().get("original-filename"), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
String contentType = headResponse.contentType() != null
|
||||||
|
? headResponse.contentType()
|
||||||
|
: MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
|
|
||||||
|
GetObjectRequest getRequest = GetObjectRequest.builder()
|
||||||
|
.bucket(properties.bucket())
|
||||||
|
.key(objectName)
|
||||||
|
.build();
|
||||||
|
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(getRequest);
|
||||||
|
|
||||||
|
log.info("Файл '{}' (оригинальное имя: '{}') успешно скачан.", objectName, originalFilename);
|
||||||
|
return new DownloadedFile(originalFilename, contentType, responseBytes.asByteArray());
|
||||||
|
|
||||||
|
} catch (NoSuchKeyException e) {
|
||||||
|
log.warn("Файл не найден: {}", objectName);
|
||||||
|
throw new ApiException(ErrorCode.NOT_FOUND, "Файл не найден.", HttpStatus.NOT_FOUND);
|
||||||
|
} catch (ApiException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Ошибка при скачивании файла '{}': {}", objectName, e.getMessage(), e);
|
||||||
|
throw new ApiException(ErrorCode.INTERNAL_ERROR, "Ошибка при скачивании файла.", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFile(MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
log.warn("Попытка загрузить пустой файл.");
|
||||||
|
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Файл не может быть пустым.", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.getSize() > MAX_FILE_SIZE) {
|
||||||
|
log.warn("Файл превышает допустимый размер: {} байт.", file.getSize());
|
||||||
|
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Файл слишком большой (максимум 10 МБ).", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
|
||||||
|
log.warn("Недопустимый тип файла: {}", contentType);
|
||||||
|
throw new ApiException(ErrorCode.VALIDATION_ERROR, "Недопустимый тип файла.", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractExtension(String originalFilename) {
|
||||||
|
if (originalFilename != null && originalFilename.contains(".")) {
|
||||||
|
return originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.krylov.refound.ai.facade.ContentModerationFacade;
|
import com.krylov.refound.ai.facade.ContentModerationFacade;
|
||||||
import com.krylov.refound.dto.PostRequest;
|
import com.krylov.refound.dto.PostRequest;
|
||||||
import com.krylov.refound.dto.PostResponse;
|
import com.krylov.refound.dto.PostResponse;
|
||||||
@@ -9,6 +11,7 @@ import com.krylov.refound.entity.User;
|
|||||||
import com.krylov.refound.enums.ErrorCode;
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
import com.krylov.refound.enums.PostCategory;
|
import com.krylov.refound.enums.PostCategory;
|
||||||
import com.krylov.refound.enums.PostStatus;
|
import com.krylov.refound.enums.PostStatus;
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
import com.krylov.refound.exception.ApiException;
|
import com.krylov.refound.exception.ApiException;
|
||||||
import com.krylov.refound.mapper.PostMapper;
|
import com.krylov.refound.mapper.PostMapper;
|
||||||
import com.krylov.refound.repository.FavoriteRepository;
|
import com.krylov.refound.repository.FavoriteRepository;
|
||||||
@@ -20,6 +23,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -52,6 +56,7 @@ public class PostService {
|
|||||||
private final FileStorageService fileStorageService;
|
private final FileStorageService fileStorageService;
|
||||||
private final ContentModerationFacade contentModerationFacade;
|
private final ContentModerationFacade contentModerationFacade;
|
||||||
private final StatisticsService statisticsService;
|
private final StatisticsService statisticsService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public PostRequest create(PostRequest request, List<MultipartFile> images) {
|
public PostRequest create(PostRequest request, List<MultipartFile> images) {
|
||||||
|
|
||||||
@@ -153,6 +158,35 @@ public class PostService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|
||||||
|
PostRequest request = new PostRequest();
|
||||||
|
request.setType(PostType.from(type));
|
||||||
|
request.setTitle(title);
|
||||||
|
request.setDescription(description);
|
||||||
|
request.setCategory(category);
|
||||||
|
request.setPhone(phone);
|
||||||
|
request.setLatitude(latitude);
|
||||||
|
request.setLongitude(longitude);
|
||||||
|
request.setAddress(address);
|
||||||
|
|
||||||
|
List<String> existingUrls = parseExistingImages(existingImagesJson);
|
||||||
|
|
||||||
|
return update(id, request, images, existingUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
// старый метод остаётся как есть — рефакторить будем позже
|
||||||
public PostResponse update(Long id, PostRequest request, List<MultipartFile> images,
|
public PostResponse update(Long id, PostRequest request, List<MultipartFile> images,
|
||||||
List<String> existingUrls) {
|
List<String> existingUrls) {
|
||||||
Post post = repository.findById(id)
|
Post post = repository.findById(id)
|
||||||
@@ -217,6 +251,21 @@ public class PostService {
|
|||||||
return mapper.toResponse(updated);
|
return mapper.toResponse(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> parseExistingImages(String existingImagesJson) {
|
||||||
|
if (existingImagesJson == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Arrays.asList(objectMapper.readValue(existingImagesJson, String[].class));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new ApiException(
|
||||||
|
ErrorCode.VALIDATION_ERROR,
|
||||||
|
"Некорректный формат existingImages",
|
||||||
|
HttpStatus.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CacheEvict(value = {"posts", "post"}, allEntries = true)
|
@CacheEvict(value = {"posts", "post"}, allEntries = true)
|
||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
Post post = repository.findById(id)
|
Post post = repository.findById(id)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserService {
|
public class UserService {
|
||||||
private final UserRepository repository;
|
private final UserRepository repository;
|
||||||
private final AvatarService avatarService;
|
private final FileStorageService fileStorageService;
|
||||||
|
|
||||||
public User getCurrentUser() {
|
public User getCurrentUser() {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
@@ -55,8 +55,19 @@ public class UserService {
|
|||||||
|
|
||||||
MultipartFile avatar = dto.getAvatar();
|
MultipartFile avatar = dto.getAvatar();
|
||||||
if (avatar != null && !avatar.isEmpty()) {
|
if (avatar != null && !avatar.isEmpty()) {
|
||||||
String url = avatarService.save(avatar); // сохранение на диск/в S3/Yandex Object Storage
|
String oldAvatarKey = user.getAvatarUrl();
|
||||||
user.setAvatarUrl(url);
|
|
||||||
|
String newAvatarKey = fileStorageService.uploadFile(avatar, "avatars");
|
||||||
|
user.setAvatarUrl(newAvatarKey);
|
||||||
|
|
||||||
|
// подчищаем старый файл, если он был и это реально ключ MinIO
|
||||||
|
if (oldAvatarKey != null && !oldAvatarKey.isBlank()) {
|
||||||
|
try {
|
||||||
|
fileStorageService.deleteFile(oldAvatarKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Не удалось удалить старый аватар из хранилища: {}", oldAvatarKey, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.save(user);
|
repository.save(user);
|
||||||
|
|||||||
Reference in New Issue
Block a user