diff --git a/pom.xml b/pom.xml
index 03bc446d..c8b0b139 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,7 +15,7 @@
SpringToDo
- 21
+ 17
@@ -23,12 +23,91 @@
org.springframework.boot
spring-boot-starter
-
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+ org.skyscreamer
+ jsonassert
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.projectlombok
+ lombok
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
org.springframework.boot
spring-boot-starter-test
test
+
+ org.liquibase
+ liquibase-core
+
+
+
+ io.micrometer
+ micrometer-core
+ 1.16.0-M2
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+ org.mapstruct
+ mapstruct
+ 1.6.3
+
@@ -37,6 +116,33 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+ 17
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.32
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
diff --git a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
index 41980514..e50bb340 100644
--- a/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
+++ b/src/main/java/com/emobile/springtodo/SpringToDoApplication.java
@@ -5,9 +5,7 @@
@SpringBootApplication
public class SpringToDoApplication {
-
public static void main(String[] args) {
SpringApplication.run(SpringToDoApplication.class, args);
}
-
}
diff --git a/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java b/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java
new file mode 100644
index 00000000..5f2f2f4e
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/configuration/RedisConfig.java
@@ -0,0 +1,71 @@
+package com.emobile.springtodo.configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+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;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+
+@Configuration
+@EnableCaching
+public class RedisConfig {
+
+ @Bean
+ public GenericJackson2JsonRedisSerializer redisJsonSerializer() {
+ ObjectMapper om = JsonMapper.builder()
+ .findAndAddModules() // JavaTimeModule и др.
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .build();
+
+ // Важно: включаем type info, чтобы при чтении восстанавливался TaskResponse, а не LinkedHashMap
+ om.activateDefaultTyping(
+ BasicPolymorphicTypeValidator.builder()
+ .allowIfSubType("com.emobile.springtodo") // ограничь своими пакетами
+ .allowIfSubType("java.util")
+ .build(),
+ ObjectMapper.DefaultTyping.NON_FINAL
+ );
+
+ return new GenericJackson2JsonRedisSerializer(om);
+ }
+
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory cf,
+ GenericJackson2JsonRedisSerializer jsonSer) {
+ RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSer))
+ .disableCachingNullValues()
+ .computePrefixWith(name -> "todo:" + name + "::")
+ .entryTtl(Duration.ofMinutes(30));
+
+ return RedisCacheManager.builder(cf)
+ .cacheDefaults(base)
+ .transactionAware()
+ .build();
+ }
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory cf,
+ GenericJackson2JsonRedisSerializer jsonSer) {
+ RedisTemplate tpl = new RedisTemplate<>();
+ tpl.setConnectionFactory(cf);
+ tpl.setKeySerializer(new StringRedisSerializer());
+ tpl.setValueSerializer(jsonSer);
+ tpl.setHashKeySerializer(new StringRedisSerializer());
+ tpl.setHashValueSerializer(jsonSer);
+ return tpl;
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/controllers/ControllerAdvice.java b/src/main/java/com/emobile/springtodo/controllers/ControllerAdvice.java
new file mode 100644
index 00000000..81a1d251
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controllers/ControllerAdvice.java
@@ -0,0 +1,17 @@
+package com.emobile.springtodo.controllers;
+
+import com.emobile.springtodo.dto.ApiResponse;
+import com.emobile.springtodo.exceptions.TaskNotFoundException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import com.emobile.springtodo.dto.ErrorResponse;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@org.springframework.web.bind.annotation.ControllerAdvice
+public class ControllerAdvice {
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(TaskNotFoundException.class)
+ public ApiResponse TaskNotFoundExceptionHandler(TaskNotFoundException exception) {
+ return ApiResponse.error(new ErrorResponse("ACCESS_ERROR", exception.getMessage()));
+ }
+}
diff --git a/src/main/java/com/emobile/springtodo/controllers/GlobalExceptionHandler.java b/src/main/java/com/emobile/springtodo/controllers/GlobalExceptionHandler.java
new file mode 100644
index 00000000..1c12197e
--- /dev/null
+++ b/src/main/java/com/emobile/springtodo/controllers/GlobalExceptionHandler.java
@@ -0,0 +1,92 @@
+package com.emobile.springtodo.controllers;
+
+import com.emobile.springtodo.dto.ApiResponse;
+import com.emobile.springtodo.dto.ErrorResponse;
+import jakarta.validation.ConstraintViolationException;
+import org.springframework.http.*;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.WebRequest;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestControllerAdvice
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ protected ApiResponse handleMethodArgumentNotValid(
+ MethodArgumentNotValidException ex) {
+
+ List