28
build.gradle
28
build.gradle
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '4.0.4'
|
id 'org.springframework.boot' version '3.2.5'
|
||||||
id 'io.spring.dependency-management' version '1.1.7'
|
id 'io.spring.dependency-management' version '1.1.7'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,22 +25,26 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-liquibase'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-session-data-redis'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
implementation 'org.liquibase:liquibase-core'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||||
|
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
|
||||||
|
|
||||||
|
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
|
||||||
|
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
|
||||||
|
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
|
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-liquibase-test'
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-session-data-redis-test'
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-validation-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
|
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
|
|||||||
@@ -1 +1,16 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: nahodka
|
||||||
|
POSTGRES_USER: user
|
||||||
|
POSTGRES_PASSWORD: pass
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
10
src/main/java/com/krylov/refound/config/CacheConfig.java
Normal file
10
src/main/java/com/krylov/refound/config/CacheConfig.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.krylov.refound.config;
|
||||||
|
|
||||||
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@EnableCaching
|
||||||
|
@Configuration
|
||||||
|
public class CacheConfig {
|
||||||
|
|
||||||
|
}
|
||||||
27
src/main/java/com/krylov/refound/config/RedisConfig.java
Normal file
27
src/main/java/com/krylov/refound/config/RedisConfig.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.krylov.refound.config;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
@Value("${cache.ttl}")
|
||||||
|
private long ttl;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
|
||||||
|
|
||||||
|
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
||||||
|
.entryTtl(Duration.ofSeconds(ttl))
|
||||||
|
.disableCachingNullValues();
|
||||||
|
|
||||||
|
return RedisCacheManager.builder(factory)
|
||||||
|
.cacheDefaults(config)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,39 @@
|
|||||||
package com.krylov.refound.config;
|
package com.krylov.refound.config;
|
||||||
|
|
||||||
|
import com.krylov.refound.service.security.JwtFilter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
private final JwtFilter jwtFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
|
http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/java/com/krylov/refound/config/WebConfig.java
Normal file
19
src/main/java/com/krylov/refound/config/WebConfig.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/java/com/krylov/refound/config/WebSocketConfig.java
Normal file
27
src/main/java/com/krylov/refound/config/WebSocketConfig.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.krylov.refound.config;
|
||||||
|
|
||||||
|
import com.krylov.refound.util.JwtHandshakeInterceptor;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||||
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocketMessageBroker
|
||||||
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
|
config.enableSimpleBroker("/topic");
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
|
registry.addEndpoint("/ws")
|
||||||
|
.setAllowedOriginPatterns("*")
|
||||||
|
.addInterceptors(new JwtHandshakeInterceptor()) // 🔥 JWT
|
||||||
|
.withSockJS();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,27 @@
|
|||||||
package com.krylov.refound.controller;
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.AuthRequest;
|
||||||
|
import com.krylov.refound.dto.AuthResponse;
|
||||||
|
import com.krylov.refound.service.security.AuthService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
private final AuthService service;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public AuthResponse register(@RequestBody AuthRequest request) {
|
||||||
|
return service.register(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
public AuthResponse login(@RequestBody AuthRequest request) {
|
||||||
|
return service.login(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.ChatMessage;
|
||||||
|
import com.krylov.refound.entity.Message;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.repository.UserRepository;
|
||||||
|
import com.krylov.refound.service.MessageService;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Optional;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.messaging.handler.annotation.Payload;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChatController {
|
||||||
|
private final MessageService service;
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
|
||||||
|
@MessageMapping("/chat")
|
||||||
|
public void send(@Payload ChatMessage message, Principal principal) {
|
||||||
|
|
||||||
|
Optional<User> sender = userRepository.findByEmail(principal.getName());
|
||||||
|
Message saved = new Message();
|
||||||
|
if(sender.isPresent()){
|
||||||
|
saved = service.send(message.getReceiverId(), message.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
// отправка получателю
|
||||||
|
messagingTemplate.convertAndSend("/topic/messages/" + message.getReceiverId(), saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.PostResponse;
|
||||||
|
import com.krylov.refound.mapper.PostMapper;
|
||||||
|
import com.krylov.refound.service.FavoriteService;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/favorites")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FavoriteController {
|
||||||
|
|
||||||
|
private final FavoriteService service;
|
||||||
|
private final PostMapper mapper;
|
||||||
|
|
||||||
|
@PostMapping("/{postId}/toggle")
|
||||||
|
public void toggle(@PathVariable Long postId) {
|
||||||
|
service.toggle(postId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<PostResponse> getAll() {
|
||||||
|
return service.getFavorites()
|
||||||
|
.stream()
|
||||||
|
.map(mapper::toResponse)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.service.FileStorageService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/files")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FileController {
|
||||||
|
private final FileStorageService storageService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public String upload(@RequestParam("file") MultipartFile file) {
|
||||||
|
return storageService.save(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.DialogResponse;
|
||||||
|
import com.krylov.refound.entity.Message;
|
||||||
|
import com.krylov.refound.service.MessageService;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/messages")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageController {
|
||||||
|
private final MessageService service;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public void send(@RequestParam Long receiverId,
|
||||||
|
@RequestParam String content) {
|
||||||
|
service.send(receiverId, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{userId}")
|
||||||
|
public List<Message> chat(@PathVariable Long userId) {
|
||||||
|
return service.getChat(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dialogs")
|
||||||
|
public List<DialogResponse> getDialogs() {
|
||||||
|
return service.getDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/read/{userId}")
|
||||||
|
public void markAsRead(@PathVariable Long userId) {
|
||||||
|
service.markAsRead(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,50 @@
|
|||||||
package com.krylov.refound.controller;
|
package com.krylov.refound.controller;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.PostFilter;
|
||||||
|
import com.krylov.refound.dto.PostRequest;
|
||||||
|
import com.krylov.refound.dto.PostResponse;
|
||||||
|
import com.krylov.refound.repository.PostRepository;
|
||||||
|
import com.krylov.refound.service.PostService;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestPart;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/posts")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class PostController {
|
public class PostController {
|
||||||
|
|
||||||
|
private final PostService service;
|
||||||
|
|
||||||
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public PostResponse create(@RequestPart("data") PostRequest request, @RequestPart(value = "files", required = false) List<MultipartFile> files) {
|
||||||
|
return service.create(request, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public Page<PostResponse> getFeed(PostFilter filter, Pageable pageable) {
|
||||||
|
return service.getFeed(filter, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public PostResponse update(@PathVariable Long id, @RequestBody PostRequest request) {
|
||||||
|
return service.update(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public void delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
package com.krylov.refound.dto;
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
public class AuthRequest {
|
public class AuthRequest {
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
package com.krylov.refound.dto;
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
public class AuthResponse {
|
public class AuthResponse {
|
||||||
|
|
||||||
|
private String token;
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/main/java/com/krylov/refound/dto/ChatMessage.java
Normal file
9
src/main/java/com/krylov/refound/dto/ChatMessage.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChatMessage {
|
||||||
|
private Long receiverId;
|
||||||
|
private String content;
|
||||||
|
}
|
||||||
15
src/main/java/com/krylov/refound/dto/DialogResponse.java
Normal file
15
src/main/java/com/krylov/refound/dto/DialogResponse.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DialogResponse {
|
||||||
|
private Long userId;
|
||||||
|
private String userEmail;
|
||||||
|
private String lastMessage;
|
||||||
|
private LocalDateTime lastMessageTime;
|
||||||
|
private Long unreadCount;
|
||||||
|
}
|
||||||
29
src/main/java/com/krylov/refound/dto/PostFilter.java
Normal file
29
src/main/java/com/krylov/refound/dto/PostFilter.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PostFilter {
|
||||||
|
|
||||||
|
private PostType type;
|
||||||
|
private String city;
|
||||||
|
private String category;
|
||||||
|
private String district;
|
||||||
|
private String search;
|
||||||
|
private Double lat;
|
||||||
|
private Double lng;
|
||||||
|
private Double radius; // в км
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "type=" + type +
|
||||||
|
",city=" + city +
|
||||||
|
",category=" + category +
|
||||||
|
",district=" + district +
|
||||||
|
",search=" + search +
|
||||||
|
",lat=" + lat +
|
||||||
|
",lng=" + lng +
|
||||||
|
",radius=" + radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,25 @@
|
|||||||
package com.krylov.refound.dto;
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
public class PostRequest {
|
public class PostRequest {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private PostType type;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
private String district;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
package com.krylov.refound.dto;
|
package com.krylov.refound.dto;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.PostStatus;
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
public class PostResponse {
|
public class PostResponse {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String category;
|
||||||
|
private String city;
|
||||||
|
private String district;
|
||||||
|
private PostType type;
|
||||||
|
private PostStatus status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private String userEmail;
|
||||||
|
private boolean isFavorite;
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/main/java/com/krylov/refound/entity/Favorite.java
Normal file
27
src/main/java/com/krylov/refound/entity/Favorite.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.krylov.refound.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.UniqueConstraint;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "favorites", uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "post_id"}))
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Favorite {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Post post;
|
||||||
|
}
|
||||||
@@ -1,4 +1,25 @@
|
|||||||
package com.krylov.refound.entity;
|
package com.krylov.refound.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "images")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class Image {
|
public class Image {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private Post post;
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/main/java/com/krylov/refound/entity/Message.java
Normal file
35
src/main/java/com/krylov/refound/entity/Message.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.krylov.refound.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "messages")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class Message {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private User sender;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private User receiver;
|
||||||
|
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private boolean isRead;
|
||||||
|
}
|
||||||
@@ -1,4 +1,51 @@
|
|||||||
package com.krylov.refound.entity;
|
package com.krylov.refound.entity;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.PostStatus;
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "posts")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class Post {
|
public class Post {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private PostType type;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
private String district;
|
||||||
|
|
||||||
|
private Double latitude;
|
||||||
|
private Double longitude;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private PostStatus status;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
private User user;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,27 @@
|
|||||||
package com.krylov.refound.entity;
|
package com.krylov.refound.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
public class User {
|
public class User {
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String email;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
package com.krylov.refound.enums;
|
package com.krylov.refound.enums;
|
||||||
|
|
||||||
public enum ErrorCode {
|
public enum ErrorCode {
|
||||||
|
VALIDATION_ERROR,
|
||||||
|
NOT_FOUND,
|
||||||
|
INTERNAL_ERROR,
|
||||||
|
BAD_REQUEST
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.krylov.refound.enums;
|
package com.krylov.refound.enums;
|
||||||
|
|
||||||
public enum PostStatus {
|
public enum PostStatus {
|
||||||
|
ACTIVE,
|
||||||
|
CLOSED
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.krylov.refound.enums;
|
package com.krylov.refound.enums;
|
||||||
|
|
||||||
public enum PostType {
|
public enum PostType {
|
||||||
|
LOST,
|
||||||
|
FOUND
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
package com.krylov.refound.exception;
|
package com.krylov.refound.exception;
|
||||||
|
|
||||||
public class ApiException {
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class ApiException extends RuntimeException {
|
||||||
|
|
||||||
|
private final ErrorCode errorCode;
|
||||||
|
private final HttpStatus status;
|
||||||
|
|
||||||
|
public ApiException(ErrorCode errorCode, String message, HttpStatus status) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
package com.krylov.refound.exception;
|
package com.krylov.refound.exception;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
public class ErrorResponse {
|
public class ErrorResponse {
|
||||||
|
private LocalDateTime timestamp;
|
||||||
|
private int status;
|
||||||
|
private String error;
|
||||||
|
private String message;
|
||||||
|
private String path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,56 @@
|
|||||||
package com.krylov.refound.exception;
|
package com.krylov.refound.exception;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
@Slf4j
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(ApiException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleApiException(ApiException ex, HttpServletRequest request) {
|
||||||
|
return buildResponse(ex.getStatus(), ex.getErrorCode(), ex.getMessage(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) {
|
||||||
|
String message = ex.getBindingResult()
|
||||||
|
.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.findFirst()
|
||||||
|
.map(e -> e.getField() + " " + e.getDefaultMessage())
|
||||||
|
.orElse("Validation error");
|
||||||
|
|
||||||
|
return buildResponse(HttpStatus.BAD_REQUEST, ErrorCode.VALIDATION_ERROR, message, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleUnknown(Exception ex, HttpServletRequest request) {
|
||||||
|
log.error("Unexpected error", ex);
|
||||||
|
|
||||||
|
return buildResponse(
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
ErrorCode.INTERNAL_ERROR,
|
||||||
|
"Internal server error",
|
||||||
|
request
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<ErrorResponse> buildResponse(HttpStatus status, ErrorCode code, String message, HttpServletRequest request) {
|
||||||
|
ErrorResponse response = ErrorResponse.builder()
|
||||||
|
.timestamp(LocalDateTime.now())
|
||||||
|
.status(status.value())
|
||||||
|
.error(code.name())
|
||||||
|
.message(message)
|
||||||
|
.path(request.getRequestURI())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new ResponseEntity<>(response, status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
package com.krylov.refound.mapper;
|
package com.krylov.refound.mapper;
|
||||||
|
|
||||||
public class PostMapper {
|
import com.krylov.refound.dto.PostRequest;
|
||||||
|
import com.krylov.refound.dto.PostResponse;
|
||||||
|
import com.krylov.refound.entity.Post;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface PostMapper {
|
||||||
|
|
||||||
|
Post toEntity(PostRequest request);
|
||||||
|
|
||||||
|
@Mapping(source = "user.email", target = "userEmail")
|
||||||
|
PostResponse toResponse(Post post);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
|
import com.krylov.refound.entity.Favorite;
|
||||||
|
import com.krylov.refound.entity.Post;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
|
||||||
|
|
||||||
|
boolean existsByUserAndPost(User user, Post post);
|
||||||
|
|
||||||
|
List<Favorite> findByUser(User user);
|
||||||
|
|
||||||
|
void deleteByUserAndPost(User user, Post post);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT f.post.id FROM Favorite f
|
||||||
|
WHERE f.user.id = :userId
|
||||||
|
AND f.post.id IN :postIds
|
||||||
|
""")
|
||||||
|
List<Long> findFavoritePostIds(Long userId, List<Long> postIds);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
|
import com.krylov.refound.entity.Image;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface ImageRepository extends JpaRepository<Image, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.DialogResponse;
|
||||||
|
import com.krylov.refound.entity.Message;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
public interface MessageRepository extends JpaRepository<Message, Long> {
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT m FROM Message m
|
||||||
|
WHERE (m.sender.id = :user1 AND m.receiver.id = :user2)
|
||||||
|
OR (m.sender.id = :user2 AND m.receiver.id = :user1)
|
||||||
|
ORDER BY m.createdAt
|
||||||
|
""")
|
||||||
|
List<Message> findChat(Long user1, Long user2);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT m FROM Message m
|
||||||
|
WHERE m.id IN (
|
||||||
|
SELECT MAX(m2.id)
|
||||||
|
FROM Message m2
|
||||||
|
WHERE m2.sender.id = :userId OR m2.receiver.id = :userId
|
||||||
|
GROUP BY
|
||||||
|
CASE
|
||||||
|
WHEN m2.sender.id = :userId THEN m2.receiver.id
|
||||||
|
ELSE m2.sender.id
|
||||||
|
END
|
||||||
|
)
|
||||||
|
ORDER BY m.createdAt DESC
|
||||||
|
""")
|
||||||
|
List<Message> findDialogs(Long userId);
|
||||||
|
|
||||||
|
// 🔥 количество непрочитанных от пользователя
|
||||||
|
@Query("""
|
||||||
|
SELECT COUNT(m) FROM Message m
|
||||||
|
WHERE m.receiver.id = :userId
|
||||||
|
AND m.sender.id = :otherUserId
|
||||||
|
AND m.isRead = false
|
||||||
|
""")
|
||||||
|
int countUnread(Long userId, Long otherUserId);
|
||||||
|
|
||||||
|
// 🔥 пометить как прочитанные
|
||||||
|
@Modifying
|
||||||
|
@Query("""
|
||||||
|
UPDATE Message m
|
||||||
|
SET m.isRead = true
|
||||||
|
WHERE m.receiver.id = :userId
|
||||||
|
AND m.sender.id = :otherUserId
|
||||||
|
AND m.isRead = false
|
||||||
|
""")
|
||||||
|
void markAsRead(Long userId, Long otherUserId);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT new com.krylov.refound.dto.DialogResponse(
|
||||||
|
CAST(
|
||||||
|
CASE
|
||||||
|
WHEN m.sender.id = :userId THEN m.receiver.id
|
||||||
|
ELSE m.sender.id
|
||||||
|
END AS long
|
||||||
|
),
|
||||||
|
CASE
|
||||||
|
WHEN m.sender.id = :userId THEN m.receiver.email
|
||||||
|
ELSE m.sender.email
|
||||||
|
END,
|
||||||
|
m.content,
|
||||||
|
m.createdAt,
|
||||||
|
SUM(CASE WHEN m.receiver.id = :userId AND m.isRead = false THEN 1 ELSE 0 END)
|
||||||
|
)
|
||||||
|
FROM Message m
|
||||||
|
WHERE m.sender.id = :userId OR m.receiver.id = :userId
|
||||||
|
GROUP BY
|
||||||
|
CASE WHEN m.sender.id = :userId THEN m.receiver.id ELSE m.sender.id END,
|
||||||
|
CASE WHEN m.sender.id = :userId THEN m.receiver.email ELSE m.sender.email END,
|
||||||
|
m.content,
|
||||||
|
m.createdAt
|
||||||
|
""")
|
||||||
|
List<DialogResponse> findDialogsWithUnread(Long userId);
|
||||||
|
}
|
||||||
@@ -1,4 +1,45 @@
|
|||||||
package com.krylov.refound.repository;
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
public interface PostRepository {
|
import com.krylov.refound.entity.Post;
|
||||||
|
import com.krylov.refound.enums.PostType;
|
||||||
|
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.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
|
||||||
|
public interface PostRepository extends JpaRepository<Post, Long>, JpaSpecificationExecutor<Post> {
|
||||||
|
|
||||||
|
Page<Post> findByType(PostType type, Pageable pageable);
|
||||||
|
|
||||||
|
@Query(value = """
|
||||||
|
SELECT * FROM Posts p
|
||||||
|
WHERE
|
||||||
|
(:type IS NULL OR p.type = :type)
|
||||||
|
AND (:city IS NULL OR p.city = :city)
|
||||||
|
AND (:category IS NULL OR p.category = :category)
|
||||||
|
AND (
|
||||||
|
:lat IS NULL OR :lng IS NULL OR :radius IS NULL OR
|
||||||
|
(
|
||||||
|
6371 * acos(
|
||||||
|
cos(radians(:lat)) * cos(radians(p.latitude)) *
|
||||||
|
cos(radians(p.longitude) - radians(:lng)) +
|
||||||
|
sin(radians(:lat)) * sin(radians(p.latitude))
|
||||||
|
)
|
||||||
|
) <= :radius
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
countQuery = "SELECT count(*) FROM Posts p",
|
||||||
|
nativeQuery = true)
|
||||||
|
Page<Post> searchWithGeo(
|
||||||
|
@Param("type") String type,
|
||||||
|
@Param("city") String city,
|
||||||
|
@Param("category") String category,
|
||||||
|
@Param("lat") Double lat,
|
||||||
|
@Param("lng") Double lng,
|
||||||
|
@Param("radius") Double radius,
|
||||||
|
Pageable pageable
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
package com.krylov.refound.repository;
|
package com.krylov.refound.repository;
|
||||||
|
|
||||||
public interface UserRepository {
|
import com.krylov.refound.entity.User;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.entity.Favorite;
|
||||||
|
import com.krylov.refound.entity.Post;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import com.krylov.refound.repository.FavoriteRepository;
|
||||||
|
import com.krylov.refound.repository.PostRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FavoriteService {
|
||||||
|
private final FavoriteRepository repository;
|
||||||
|
private final PostRepository postRepository;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public void addToFavorites(Long postId) {
|
||||||
|
|
||||||
|
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)
|
||||||
|
.stream()
|
||||||
|
.map(Favorite::getPost)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle(Long postId) {
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
repository.deleteByUserAndPost(user, post);
|
||||||
|
} else {
|
||||||
|
Favorite f = new Favorite();
|
||||||
|
f.setUser(user);
|
||||||
|
f.setPost(post);
|
||||||
|
repository.save(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class FileStorageService {
|
||||||
|
|
||||||
|
@Value("${file.upload-dir}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
public String save(MultipartFile file) {
|
||||||
|
try {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
throw new RuntimeException("File is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||||
|
|
||||||
|
Path path = Paths.get(uploadDir).toAbsolutePath().normalize();
|
||||||
|
Files.createDirectories(path);
|
||||||
|
|
||||||
|
Path target = path.resolve(fileName);
|
||||||
|
|
||||||
|
Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
return "/files/" + fileName; // URL
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ApiException(ErrorCode.BAD_REQUEST, "Only image files are allowed", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main/java/com/krylov/refound/service/MessageService.java
Normal file
58
src/main/java/com/krylov/refound/service/MessageService.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.DialogResponse;
|
||||||
|
import com.krylov.refound.entity.Message;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import com.krylov.refound.repository.MessageRepository;
|
||||||
|
import com.krylov.refound.repository.UserRepository;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageService {
|
||||||
|
private final MessageRepository repository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
public Message send(Long receiverId, String content) {
|
||||||
|
|
||||||
|
User sender = userService.getCurrentUser();
|
||||||
|
|
||||||
|
User receiver = userRepository.findById(receiverId)
|
||||||
|
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "User not found", HttpStatus.NOT_FOUND));
|
||||||
|
|
||||||
|
Message message = new Message();
|
||||||
|
message.setSender(sender);
|
||||||
|
message.setReceiver(receiver);
|
||||||
|
message.setContent(content);
|
||||||
|
message.setCreatedAt(LocalDateTime.now());
|
||||||
|
message.setRead(false);
|
||||||
|
|
||||||
|
return repository.save(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Message> getChat(Long userId) {
|
||||||
|
User current = userService.getCurrentUser();
|
||||||
|
return repository.findChat(current.getId(), userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DialogResponse> getDialogs() {
|
||||||
|
User current = userService.getCurrentUser();
|
||||||
|
return repository.findDialogsWithUnread(current.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void markAsRead(Long otherUserId) {
|
||||||
|
|
||||||
|
User current = userService.getCurrentUser();
|
||||||
|
|
||||||
|
repository.markAsRead(current.getId(), otherUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,149 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.PostFilter;
|
||||||
|
import com.krylov.refound.dto.PostRequest;
|
||||||
|
import com.krylov.refound.dto.PostResponse;
|
||||||
|
import com.krylov.refound.entity.Image;
|
||||||
|
import com.krylov.refound.entity.Post;
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import com.krylov.refound.enums.PostStatus;
|
||||||
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import com.krylov.refound.mapper.PostMapper;
|
||||||
|
import com.krylov.refound.repository.FavoriteRepository;
|
||||||
|
import com.krylov.refound.repository.ImageRepository;
|
||||||
|
import com.krylov.refound.repository.PostRepository;
|
||||||
|
import com.krylov.refound.util.PostSpecification;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class PostService {
|
public class PostService {
|
||||||
|
|
||||||
|
private final PostRepository repository;
|
||||||
|
private final ImageRepository imageRepository;
|
||||||
|
private final FileStorageService storageService;
|
||||||
|
private final PostMapper mapper;
|
||||||
|
private final UserService userService;
|
||||||
|
private final FavoriteRepository favoriteRepository;
|
||||||
|
|
||||||
|
@CacheEvict(value = "posts", allEntries = true)
|
||||||
|
public PostResponse create(PostRequest request, List<MultipartFile> files) {
|
||||||
|
|
||||||
|
User user = userService.getCurrentUser();
|
||||||
|
|
||||||
|
Post post = mapper.toEntity(request);
|
||||||
|
post.setCreatedAt(LocalDateTime.now());
|
||||||
|
post.setStatus(PostStatus.ACTIVE);
|
||||||
|
post.setUser(user);
|
||||||
|
|
||||||
|
Post savedPost = repository.save(post);
|
||||||
|
|
||||||
|
if (files != null) {
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
String url = storageService.save(file);
|
||||||
|
|
||||||
|
Image image = new Image();
|
||||||
|
image.setUrl(url);
|
||||||
|
image.setPost(savedPost);
|
||||||
|
|
||||||
|
imageRepository.save(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapper.toResponse(savedPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "posts", key = "#filter.toString() + '_' + #pageable.pageNumber")
|
||||||
|
public Page<PostResponse> getFeed(PostFilter filter, Pageable pageable) {
|
||||||
|
|
||||||
|
User user = userService.getCurrentUser();
|
||||||
|
|
||||||
|
Page<Post> posts;
|
||||||
|
|
||||||
|
if (filter.getLat() != null) {
|
||||||
|
posts = repository.searchWithGeo(
|
||||||
|
filter.getType() != null ? filter.getType().name() : null,
|
||||||
|
filter.getCity(),
|
||||||
|
filter.getCategory(),
|
||||||
|
filter.getLat(),
|
||||||
|
filter.getLng(),
|
||||||
|
filter.getRadius(),
|
||||||
|
pageable
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
posts = repository.findAll(PostSpecification.withFilter(filter), pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 собираем id всех постов
|
||||||
|
List<Long> postIds = posts.getContent()
|
||||||
|
.stream()
|
||||||
|
.map(Post::getId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 🔥 одним запросом получаем избранные
|
||||||
|
Set<Long> favoriteIds = new HashSet<>(
|
||||||
|
favoriteRepository.findFavoritePostIds(user.getId(), postIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
return posts.map(post -> {
|
||||||
|
PostResponse response = mapper.toResponse(post);
|
||||||
|
response.setFavorite(favoriteIds.contains(post.getId()));
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable(value = "post", key = "#id")
|
||||||
|
public PostResponse getById(Long id) {
|
||||||
|
Post post = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new ApiException(ErrorCode.NOT_FOUND, "Post not found", HttpStatus.NOT_FOUND));
|
||||||
|
|
||||||
|
return mapper.toResponse(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(value = {"posts", "post"}, allEntries = true)
|
||||||
|
public PostResponse update(Long id, PostRequest request) {
|
||||||
|
|
||||||
|
Post post = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new ApiException(
|
||||||
|
ErrorCode.NOT_FOUND,
|
||||||
|
"Post not found",
|
||||||
|
HttpStatus.NOT_FOUND
|
||||||
|
));
|
||||||
|
|
||||||
|
post.setTitle(request.getTitle());
|
||||||
|
post.setDescription(request.getDescription());
|
||||||
|
post.setCategory(request.getCategory());
|
||||||
|
post.setCity(request.getCity());
|
||||||
|
post.setDistrict(request.getDistrict());
|
||||||
|
post.setType(request.getType());
|
||||||
|
|
||||||
|
Post updated = repository.save(post);
|
||||||
|
|
||||||
|
return mapper.toResponse(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(value = {"posts", "post"}, allEntries = true)
|
||||||
|
public void delete(Long id) {
|
||||||
|
|
||||||
|
Post post = repository.findById(id)
|
||||||
|
.orElseThrow(() -> new ApiException(
|
||||||
|
ErrorCode.NOT_FOUND,
|
||||||
|
"Post not found",
|
||||||
|
HttpStatus.NOT_FOUND
|
||||||
|
));
|
||||||
|
|
||||||
|
repository.delete(post);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,27 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service;
|
||||||
|
|
||||||
|
import com.krylov.refound.entity.User;
|
||||||
|
import com.krylov.refound.enums.ErrorCode;
|
||||||
|
import com.krylov.refound.exception.ApiException;
|
||||||
|
import com.krylov.refound.repository.UserRepository;
|
||||||
|
import com.krylov.refound.util.SecurityUtils;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
private final UserRepository repository;
|
||||||
|
|
||||||
|
public User getCurrentUser() {
|
||||||
|
String email = SecurityUtils.getCurrentUserEmail();
|
||||||
|
|
||||||
|
return repository.findByEmail(email)
|
||||||
|
.orElseThrow(() -> new ApiException(
|
||||||
|
ErrorCode.NOT_FOUND,
|
||||||
|
"User not found",
|
||||||
|
HttpStatus.NOT_FOUND
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service.security;
|
||||||
|
|
||||||
import com.krylov.refound.dto.AuthRequest;
|
import com.krylov.refound.dto.AuthRequest;
|
||||||
import com.krylov.refound.dto.AuthResponse;
|
import com.krylov.refound.dto.AuthResponse;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service.security;
|
||||||
|
|
||||||
import com.krylov.refound.entity.User;
|
import com.krylov.refound.entity.User;
|
||||||
import com.krylov.refound.repository.UserRepository;
|
import com.krylov.refound.repository.UserRepository;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service.security;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.krylov.refound.service;
|
package com.krylov.refound.service.security;
|
||||||
|
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.krylov.refound.util;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
|
||||||
|
public class JwtHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beforeHandshake(
|
||||||
|
ServerHttpRequest request,
|
||||||
|
ServerHttpResponse response,
|
||||||
|
WebSocketHandler wsHandler,
|
||||||
|
Map<String, Object> attributes
|
||||||
|
) throws Exception {
|
||||||
|
|
||||||
|
List<String> auth = request.getHeaders().get("Authorization");
|
||||||
|
|
||||||
|
if (auth == null || auth.isEmpty()) {
|
||||||
|
return false; // запретить подключение
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = auth.get(0).replace("Bearer ", "");
|
||||||
|
|
||||||
|
// 🔥 проверяем JWT
|
||||||
|
String userEmail = JwtUtils.validateTokenAndGetEmail(token);
|
||||||
|
|
||||||
|
if (userEmail == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put("userEmail", userEmail);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterHandshake(
|
||||||
|
ServerHttpRequest request,
|
||||||
|
ServerHttpResponse response,
|
||||||
|
WebSocketHandler wsHandler,
|
||||||
|
Exception exception
|
||||||
|
) { }
|
||||||
|
}
|
||||||
23
src/main/java/com/krylov/refound/util/JwtUtils.java
Normal file
23
src/main/java/com/krylov/refound/util/JwtUtils.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.krylov.refound.util;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtUtils {
|
||||||
|
private static final String SECRET = "SECRET_KEY"; // заменить на env var
|
||||||
|
|
||||||
|
public static String validateTokenAndGetEmail(String token) {
|
||||||
|
try {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes()))
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody()
|
||||||
|
.getSubject();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main/java/com/krylov/refound/util/PostSpecification.java
Normal file
58
src/main/java/com/krylov/refound/util/PostSpecification.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package com.krylov.refound.util;
|
||||||
|
|
||||||
|
import com.krylov.refound.dto.PostFilter;
|
||||||
|
import com.krylov.refound.entity.Post;
|
||||||
|
import jakarta.persistence.criteria.Expression;
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
|
public class PostSpecification {
|
||||||
|
public static Specification<Post> withFilter(PostFilter filter) {
|
||||||
|
return (root, query, cb) -> {
|
||||||
|
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (filter.getType() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("type"), filter.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.getCity() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("city"), filter.getCity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.getCategory() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("category"), filter.getCategory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.getDistrict() != null) {
|
||||||
|
predicates.add(cb.equal(root.get("district"), filter.getDistrict()));
|
||||||
|
}
|
||||||
|
if (filter.getSearch() != null && !filter.getSearch().isBlank()) {
|
||||||
|
String search = "%" + filter.getSearch().toLowerCase() + "%";
|
||||||
|
|
||||||
|
predicates.add(cb.or(
|
||||||
|
cb.like(cb.lower(root.get("title")), search),
|
||||||
|
cb.like(cb.lower(root.get("description")), search)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (filter.getLat() != null && filter.getLng() != null && filter.getRadius() != null) {
|
||||||
|
|
||||||
|
Expression<Double> distance = cb.function(
|
||||||
|
"6371 * acos(cos(radians(?1)) * cos(radians(latitude)) * " +
|
||||||
|
"cos(radians(longitude) - radians(?2)) + sin(radians(?1)) * sin(radians(latitude)))",
|
||||||
|
Double.class,
|
||||||
|
cb.literal(filter.getLat()),
|
||||||
|
cb.literal(filter.getLng())
|
||||||
|
);
|
||||||
|
|
||||||
|
predicates.add(cb.lessThanOrEqualTo(distance, filter.getRadius()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
package com.krylov.refound.util;
|
package com.krylov.refound.util;
|
||||||
|
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
public class SecurityUtils {
|
public class SecurityUtils {
|
||||||
|
public static String getCurrentUserEmail() {
|
||||||
|
return SecurityContextHolder.getContext()
|
||||||
|
.getAuthentication()
|
||||||
|
.getName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
spring.application.name=ReFound
|
|
||||||
36
src/main/resources/application.yaml
Normal file
36
src/main/resources/application.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
spring:
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 5MB
|
||||||
|
max-request-size: 10MB
|
||||||
|
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/nahodka
|
||||||
|
username: user
|
||||||
|
password: pass
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update # на старте удобно
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
cache:
|
||||||
|
type: redis
|
||||||
|
|
||||||
|
liquibase:
|
||||||
|
change-log: classpath:db/changelog/db.changelog-master.yaml
|
||||||
|
|
||||||
|
file:
|
||||||
|
upload-dir: uploads/
|
||||||
|
|
||||||
|
cache:
|
||||||
|
ttl: 300
|
||||||
57
src/main/resources/db/changelog/001-init.yaml
Normal file
57
src/main/resources/db/changelog/001-init.yaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 001-create-users
|
||||||
|
author: you
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: users
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGSERIAL, constraints: { primaryKey: true } }
|
||||||
|
- column: { name: email, type: VARCHAR(255), constraints: { nullable: false, unique: true } }
|
||||||
|
- column: { name: password, type: VARCHAR(255), constraints: { nullable: false } }
|
||||||
|
- column: { name: created_at, type: TIMESTAMP }
|
||||||
|
|
||||||
|
- changeSet:
|
||||||
|
id: 002-create-posts
|
||||||
|
author: you
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: posts
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGSERIAL, constraints: { primaryKey: true } }
|
||||||
|
- column: { name: type, type: VARCHAR(20) }
|
||||||
|
- column: { name: title, type: VARCHAR(255) }
|
||||||
|
- column: { name: description, type: TEXT }
|
||||||
|
- column: { name: category, type: VARCHAR(100) }
|
||||||
|
- column: { name: city, type: VARCHAR(100) }
|
||||||
|
- column: { name: district, type: VARCHAR(100) }
|
||||||
|
- column: { name: latitude, type: DOUBLE }
|
||||||
|
- column: { name: longitude, type: DOUBLE }
|
||||||
|
- column: { name: status, type: VARCHAR(20) }
|
||||||
|
- column: { name: created_at, type: TIMESTAMP }
|
||||||
|
- column: { name: user_id, type: BIGINT }
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: posts
|
||||||
|
baseColumnNames: user_id
|
||||||
|
referencedTableName: users
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_posts_user # ✅ ДОБАВИТЬ
|
||||||
|
|
||||||
|
- changeSet:
|
||||||
|
id: 003-create-images
|
||||||
|
author: you
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: images
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGSERIAL, constraints: { primaryKey: true } }
|
||||||
|
- column: { name: url, type: VARCHAR(500) }
|
||||||
|
- column: { name: post_id, type: BIGINT }
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: images
|
||||||
|
baseColumnNames: post_id
|
||||||
|
referencedTableName: posts
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_images_post # ✅
|
||||||
37
src/main/resources/db/changelog/002-message.yaml
Normal file
37
src/main/resources/db/changelog/002-message.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 004-create-messages
|
||||||
|
author: you
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: messages
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGSERIAL, constraints: { primaryKey: true } }
|
||||||
|
- column: { name: sender_id, type: BIGINT }
|
||||||
|
- column: { name: receiver_id, type: BIGINT }
|
||||||
|
- column: { name: content, type: TEXT }
|
||||||
|
- column: { name: created_at, type: TIMESTAMP }
|
||||||
|
- column: { name: is_read, type: BOOLEAN }
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: messages
|
||||||
|
baseColumnNames: sender_id
|
||||||
|
referencedTableName: users
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_messages_sender # ✅
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: messages
|
||||||
|
baseColumnNames: receiver_id
|
||||||
|
referencedTableName: users
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_messages_receiver # ✅
|
||||||
|
|
||||||
|
# 🔥 индекс для чата
|
||||||
|
- createIndex:
|
||||||
|
tableName: messages
|
||||||
|
indexName: idx_messages_users
|
||||||
|
columns:
|
||||||
|
- column: { name: sender_id }
|
||||||
|
- column: { name: receiver_id }
|
||||||
|
- column: { name: created_at }
|
||||||
31
src/main/resources/db/changelog/003-favorite.yaml
Normal file
31
src/main/resources/db/changelog/003-favorite.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- changeSet:
|
||||||
|
id: 005-create-favorites
|
||||||
|
author: you
|
||||||
|
changes:
|
||||||
|
- createTable:
|
||||||
|
tableName: favorites
|
||||||
|
columns:
|
||||||
|
- column: { name: id, type: BIGSERIAL, constraints: { primaryKey: true } }
|
||||||
|
- column: { name: user_id, type: BIGINT }
|
||||||
|
- column: { name: post_id, type: BIGINT }
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: favorites
|
||||||
|
baseColumnNames: user_id
|
||||||
|
referencedTableName: users
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_favorites_user # ✅
|
||||||
|
|
||||||
|
- addForeignKeyConstraint:
|
||||||
|
baseTableName: favorites
|
||||||
|
baseColumnNames: post_id
|
||||||
|
referencedTableName: posts
|
||||||
|
referencedColumnNames: id
|
||||||
|
constraintName: fk_favorites_post # ✅
|
||||||
|
|
||||||
|
# 🔥 защита от дублей
|
||||||
|
- addUniqueConstraint:
|
||||||
|
tableName: favorites
|
||||||
|
columnNames: user_id, post_id
|
||||||
|
constraintName: unique_user_post
|
||||||
9
src/main/resources/db/changelog/db.changelog-master.yaml
Normal file
9
src/main/resources/db/changelog/db.changelog-master.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
databaseChangeLog:
|
||||||
|
- include:
|
||||||
|
file: db/changelog/001-init.yaml
|
||||||
|
|
||||||
|
- include:
|
||||||
|
file: db/changelog/002-message.yaml
|
||||||
|
|
||||||
|
- include:
|
||||||
|
file: db/changelog/003-favorite.yaml
|
||||||
Reference in New Issue
Block a user