diff --git a/.gitignore b/.gitignore index 549e00a2..64703cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,22 @@ -HELP.md +*.class +*.log +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +replay_pid* +out/ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/docker-compose.yml b/docker-compose.yml index b387e1a6..9f5351c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,45 @@ -version: '3.1' services: stats-server: + build: + context: stats/stats-server + dockerfile: Dockerfile + container_name: stats-server ports: - "9090:9090" + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/ewm-stats + - SPRING_DATASOURCE_USERNAME=test + - SPRING_DATASOURCE_PASSWORD=test + stats-db: - image: postgres:14-alpine + image: postgres:16.1 + container_name: postgres-stats-ewm + ports: + - "6542:5432" + environment: + - POSTGRES_DB=ewm-stats + - POSTGRES_USER=test + - POSTGRES_PASSWORD=test ewm-service: + build: + context: main-service/ + dockerfile: Dockerfile + container_name: main-server ports: - "8080:8080" + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/ewm-main + - SPRING_DATASOURCE_USERNAME=test + - SPRING_DATASOURCE_PASSWORD=test ewm-db: - image: postgres:14-alpine + image: postgres:16.1 + container_name: postgres-main-ewm + ports: + - "6543:5432" + environment: + - POSTGRES_DB=ewm-main + - POSTGRES_USER=test + - POSTGRES_PASSWORD=test \ No newline at end of file diff --git a/ewm-main-service-spec.json b/ewm-main-service-spec.json index 191bd9d4..f28d1413 100644 --- a/ewm-main-service-spec.json +++ b/ewm-main-service-spec.json @@ -1,7 +1,8 @@ { "openapi": "3.0.1", "info": { - "title": "Main service API", + "description": "Documentation \"Explore With Me\" API v1.0", + "title": "\"Explore With Me\" API сервер", "version": "1.0" }, "servers": [ @@ -479,6 +480,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -634,6 +636,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -803,6 +806,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -943,6 +947,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -1074,6 +1079,8 @@ "name": "text", "required": false, "schema": { + "maxLength": 7000, + "minLength": 1, "type": "string" } }, @@ -1146,6 +1153,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -1287,6 +1295,7 @@ "name": "from", "required": false, "schema": { + "minimum": 0, "type": "integer", "format": "int32", "default": 0 @@ -2098,6 +2107,8 @@ "example": 1 }, "name": { + "maxLength": 50, + "minLength": 1, "type": "string", "description": "Название категории", "example": "Концерты" @@ -2441,6 +2452,8 @@ "type": "object", "properties": { "name": { + "maxLength": 50, + "minLength": 1, "type": "string", "description": "Название категории", "example": "Концерты" @@ -2476,6 +2489,8 @@ "default": false }, "title": { + "maxLength": 50, + "minLength": 1, "type": "string", "description": "Заголовок подборки", "example": "Летние концерты" @@ -2559,11 +2574,15 @@ "type": "object", "properties": { "email": { + "maxLength": 254, + "minLength": 6, "type": "string", "description": "Почтовый адрес", "example": "ivan.petrov@practicummail.ru" }, "name": { + "maxLength": 250, + "minLength": 2, "type": "string", "description": "Имя", "example": "Иван Петров" @@ -2624,6 +2643,8 @@ "example": true }, "title": { + "maxLength": 50, + "minLength": 1, "type": "string", "description": "Заголовок подборки", "example": "Необычные фотозоны" diff --git a/main-service/Dockerfile b/main-service/Dockerfile new file mode 100644 index 00000000..904a3487 --- /dev/null +++ b/main-service/Dockerfile @@ -0,0 +1,3 @@ +FROM amazoncorretto:21 +COPY target/*.jar main.jar +ENTRYPOINT ["java","-jar","/main.jar"] \ No newline at end of file diff --git a/main-service/pom.xml b/main-service/pom.xml new file mode 100644 index 00000000..9bd0623d --- /dev/null +++ b/main-service/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + ru.practicum.yandex + main-service + + + 21 + 21 + UTF-8 + + + + ru.practicum.yandex + stats-client + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + ru.practicum.exploreWithMe.ExploreWithMeMain + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + \ No newline at end of file diff --git a/main-service/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeMain.java b/main-service/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeMain.java new file mode 100644 index 00000000..44de9c87 --- /dev/null +++ b/main-service/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeMain.java @@ -0,0 +1,11 @@ +package ru.practicum.exploreWithMe; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExploreWithMeMain { + public static void main(String[] args) { + SpringApplication.run(ExploreWithMeMain.class, args); + } +} diff --git a/main-service/src/main/resources/application.properties b/main-service/src/main/resources/application.properties new file mode 100644 index 00000000..7ca7a42b --- /dev/null +++ b/main-service/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 + +stats-server.url=http://localhost:9090 diff --git a/pom.xml b/pom.xml index ad386209..1d504498 100644 --- a/pom.xml +++ b/pom.xml @@ -1,16 +1,20 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent - 2.7.5 + 3.3.2 Explore With Me + + stats + main-service + ru.practicum explore-with-me @@ -18,7 +22,7 @@ pom - 11 + 21 UTF-8 @@ -63,7 +67,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.7.0.0 + 4.8.5.0 Max High @@ -79,7 +83,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.12 file diff --git a/stats/pom.xml b/stats/pom.xml new file mode 100644 index 00000000..4c84ecf3 --- /dev/null +++ b/stats/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + + + ru.practicum.yandex + stats + pom + + + stats-client + stats-dto + stats-server + + + + 21 + 21 + UTF-8 + + + \ No newline at end of file diff --git a/stats/stats-client/pom.xml b/stats/stats-client/pom.xml new file mode 100644 index 00000000..a9cf34eb --- /dev/null +++ b/stats/stats-client/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + ../../pom.xml + + + ru.practicum.yandex + stats-client + + + 21 + 21 + UTF-8 + + + + + ru.practicum.yandex + stats-dto + 0.0.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + \ No newline at end of file diff --git a/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/BaseClient.java b/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/BaseClient.java new file mode 100644 index 00000000..4098da0f --- /dev/null +++ b/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/BaseClient.java @@ -0,0 +1,61 @@ +package ru.practicum.exploreWithMe.client; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +public class BaseClient { + protected final RestTemplate rest; + + public BaseClient(RestTemplate rest) { + this.rest = rest; + } + + protected ResponseEntity post(String path, T body) { + return makeAndSendRequest(HttpMethod.POST, path, body); + } + + protected ResponseEntity get(String path) { + return makeAndSendRequest(HttpMethod.GET, path, null); + } + + private ResponseEntity makeAndSendRequest(HttpMethod method, String path, @Nullable T body) { + HttpEntity requestEntity = new HttpEntity<>(body, defaultHeaders()); + + ResponseEntity statsServerResponse; + try { + statsServerResponse = rest.exchange(path, method, requestEntity, Object.class); + } catch (HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).body(e.getResponseBodyAsByteArray()); + } + return prepareGatewayResponse(statsServerResponse); + } + + private HttpHeaders defaultHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + return headers; + } + + private static ResponseEntity prepareGatewayResponse(ResponseEntity response) { + if (response.getStatusCode().is2xxSuccessful()) { + return response; + } + + ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(response.getStatusCode()); + + if (response.hasBody()) { + return responseBuilder.body(response.getBody()); + } + + return responseBuilder.build(); + } +} diff --git a/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/StatsClient.java b/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/StatsClient.java new file mode 100644 index 00000000..76dbfc08 --- /dev/null +++ b/stats/stats-client/src/main/java/ru/practicum/exploreWithMe/client/StatsClient.java @@ -0,0 +1,42 @@ +package ru.practicum.exploreWithMe.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import ru.practicum.exploreWithMe.dto.EndpointHitDto; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class StatsClient extends BaseClient { + + public StatsClient(@Value("${stats-server.url}") String serverUrl, RestTemplateBuilder builder) { + super(builder + .uriTemplateHandler(new DefaultUriBuilderFactory(serverUrl)) + .requestFactory(() -> new HttpComponentsClientHttpRequestFactory()) + .build()); + } + + public ResponseEntity addHit(EndpointHitDto hitDto) { + return post("/hit", hitDto); + } + + public ResponseEntity getStats(LocalDateTime start, LocalDateTime end, List uris, boolean unique) { + UriComponents uriComponents = UriComponentsBuilder.newInstance() + .path("/stats") + .queryParam("start", start.toString()) + .queryParam("end", end.toString()) + .queryParam("unique", unique) + .queryParam("uris", String.join(",", uris)) + .build(); + String uri = uriComponents.toUriString(); + return get(uri); + } + +} diff --git a/stats/stats-dto/pom.xml b/stats/stats-dto/pom.xml new file mode 100644 index 00000000..1efb818f --- /dev/null +++ b/stats/stats-dto/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + ../../pom.xml + + + ru.practicum.yandex + stats-dto + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.hibernate.validator + hibernate-validator + + + org.projectlombok + lombok + + + + \ No newline at end of file diff --git a/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/EndpointHitDto.java b/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/EndpointHitDto.java new file mode 100644 index 00000000..4915d49c --- /dev/null +++ b/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/EndpointHitDto.java @@ -0,0 +1,26 @@ +package ru.practicum.exploreWithMe.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class EndpointHitDto { + private long id; + @NotBlank + private String app; + @NotBlank + private String uri; + @NotBlank + private String ip; + @PastOrPresent + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime timestamp; +} diff --git a/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/ViewStatsDto.java b/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/ViewStatsDto.java new file mode 100644 index 00000000..d953fb0e --- /dev/null +++ b/stats/stats-dto/src/main/java/ru/practicum/exploreWithMe/dto/ViewStatsDto.java @@ -0,0 +1,14 @@ +package ru.practicum.exploreWithMe.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ViewStatsDto { + private String app; + private String uri; + private long hits; +} diff --git a/stats/stats-server/Dockerfile b/stats/stats-server/Dockerfile new file mode 100644 index 00000000..1dc23499 --- /dev/null +++ b/stats/stats-server/Dockerfile @@ -0,0 +1,3 @@ +FROM amazoncorretto:21 +COPY target/*.jar stats.jar +ENTRYPOINT ["java","-jar","/stats.jar"] \ No newline at end of file diff --git a/stats/stats-server/pom.xml b/stats/stats-server/pom.xml new file mode 100644 index 00000000..fcc2522c --- /dev/null +++ b/stats/stats-server/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + ru.practicum + explore-with-me + 0.0.1-SNAPSHOT + ../../pom.xml + + + ru.practicum.yandex + stats-server + + + 21 + 21 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.projectlombok + lombok + + + ru.practicum.yandex + stats-dto + 0.0.1-SNAPSHOT + + + org.postgresql + postgresql + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + ru.practicum.exploreWithMe.ExploreWithMeStats + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeStats.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeStats.java new file mode 100644 index 00000000..2137c003 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/ExploreWithMeStats.java @@ -0,0 +1,11 @@ +package ru.practicum.exploreWithMe; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExploreWithMeStats { + public static void main(String[] args) { + SpringApplication.run(ExploreWithMeStats.class, args); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/controller/StatsController.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/controller/StatsController.java new file mode 100644 index 00000000..1910dde6 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/controller/StatsController.java @@ -0,0 +1,35 @@ +package ru.practicum.exploreWithMe.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import ru.practicum.exploreWithMe.dto.EndpointHitDto; +import ru.practicum.exploreWithMe.dto.ViewStatsDto; +import ru.practicum.exploreWithMe.service.StatsService; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class StatsController { + private final StatsService service; + + @PostMapping("/hit") + public EndpointHitDto addHit(@RequestBody @Valid EndpointHitDto hitDto) { + return service.addHit(hitDto); + } + + @GetMapping("/stats") + public List getStats(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end, + @RequestParam(required = false) List uris, + @RequestParam(required = false, defaultValue = "false") boolean unique) { + return service.getStats(start, end, uris, unique); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/mapper/Mapper.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/mapper/Mapper.java new file mode 100644 index 00000000..8ddfd301 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/mapper/Mapper.java @@ -0,0 +1,38 @@ +package ru.practicum.exploreWithMe.mapper; + +import ru.practicum.exploreWithMe.dto.EndpointHitDto; +import ru.practicum.exploreWithMe.dto.ViewStatsDto; +import ru.practicum.exploreWithMe.model.EndpointHit; +import ru.practicum.exploreWithMe.model.ViewStats; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class Mapper { + public static EndpointHit toEndpointHit(EndpointHitDto hitDto) { + EndpointHit hit = new EndpointHit(); + hit.setApp(hitDto.getApp()); + hit.setUri(hitDto.getUri()); + hit.setIp(hitDto.getIp()); + hit.setTimestamp(hitDto.getTimestamp()); + return hit; + } + + public static EndpointHitDto toEndpointHitDto(EndpointHit hit) { + return new EndpointHitDto(hit.getId(), hit.getApp(), hit.getUri(), hit.getIp(), hit.getTimestamp()); + } + + public static List toViewStatsDtoList(List stats) { + List statsDroList = new ArrayList<>(); + for (ViewStats viewStats : stats) { + statsDroList.add(toViewStatsDto(viewStats)); + } + statsDroList.sort(Comparator.comparingLong(ViewStatsDto::getHits).reversed()); + return statsDroList; + } + + private static ViewStatsDto toViewStatsDto(ViewStats viewStats) { + return new ViewStatsDto(viewStats.getApp(), viewStats.getUri(), viewStats.getHits()); + } +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/EndpointHit.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/EndpointHit.java new file mode 100644 index 00000000..fdfcb999 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/EndpointHit.java @@ -0,0 +1,32 @@ +package ru.practicum.exploreWithMe.model; + +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 lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Data +@Table(name = "endpoint_hits") +public class EndpointHit { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + @Column + private String app; + @Column + private String uri; + @Column + private String ip; + @Column(name = "time_stamp") + private LocalDateTime timestamp; +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/ViewStats.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/ViewStats.java new file mode 100644 index 00000000..ca7dc20e --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/model/ViewStats.java @@ -0,0 +1,14 @@ +package ru.practicum.exploreWithMe.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ViewStats { + private String app; + private String uri; + private long hits; +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/repository/StatsRepository.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/repository/StatsRepository.java new file mode 100644 index 00000000..8222b1c8 --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/repository/StatsRepository.java @@ -0,0 +1,37 @@ +package ru.practicum.exploreWithMe.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import ru.practicum.exploreWithMe.model.EndpointHit; +import ru.practicum.exploreWithMe.model.ViewStats; + +import java.time.LocalDateTime; +import java.util.List; + +public interface StatsRepository extends JpaRepository { + @Query("SELECT new ru.practicum.exploreWithMe.model.ViewStats(e.app, e.uri, COUNT(e)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "GROUP BY e.app, e.uri") + List findStatsBetweenDates(LocalDateTime start, LocalDateTime end); + + @Query("SELECT new ru.practicum.exploreWithMe.model.ViewStats(e.app, e.uri, COUNT(e)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "AND (:uris IS NULL OR e.uri IN :uris) " + + "GROUP BY e.app, e.uri") + List findStatsBetweenDatesAndUris(LocalDateTime start, LocalDateTime end, List uris); + + @Query("SELECT new ru.practicum.exploreWithMe.model.ViewStats(e.app, e.uri, COUNT(DISTINCT e.ip)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "GROUP BY e.app, e.uri") + List findUniqueIpStatsBetweenDates(LocalDateTime start, LocalDateTime end); + + @Query("SELECT new ru.practicum.exploreWithMe.model.ViewStats(e.app, e.uri, COUNT(DISTINCT e.ip)) " + + "FROM EndpointHit e " + + "WHERE e.timestamp BETWEEN :start AND :end " + + "AND (:uris IS NULL OR e.uri IN :uris) " + + "GROUP BY e.app, e.uri") + List findUniqueIpStatsBetweenDatesAndUris(LocalDateTime start, LocalDateTime end, List uris); +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsService.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsService.java new file mode 100644 index 00000000..c35ef68c --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsService.java @@ -0,0 +1,13 @@ +package ru.practicum.exploreWithMe.service; + +import ru.practicum.exploreWithMe.dto.EndpointHitDto; +import ru.practicum.exploreWithMe.dto.ViewStatsDto; + +import java.time.LocalDateTime; +import java.util.List; + +public interface StatsService { + EndpointHitDto addHit(EndpointHitDto endpointHitDto); + + List getStats(LocalDateTime start, LocalDateTime end, List uris, boolean unique); +} diff --git a/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsServiceImpl.java b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsServiceImpl.java new file mode 100644 index 00000000..f349696d --- /dev/null +++ b/stats/stats-server/src/main/java/ru/practicum/exploreWithMe/service/StatsServiceImpl.java @@ -0,0 +1,41 @@ +package ru.practicum.exploreWithMe.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.exploreWithMe.dto.EndpointHitDto; +import ru.practicum.exploreWithMe.dto.ViewStatsDto; +import ru.practicum.exploreWithMe.mapper.Mapper; +import ru.practicum.exploreWithMe.model.EndpointHit; +import ru.practicum.exploreWithMe.repository.StatsRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StatsServiceImpl implements StatsService { + private final StatsRepository repository; + + @Override + public EndpointHitDto addHit(EndpointHitDto endpointHitDto) { + EndpointHit savedHit = repository.save(Mapper.toEndpointHit(endpointHitDto)); + return Mapper.toEndpointHitDto(savedHit); + } + + @Override + public List getStats(LocalDateTime start, LocalDateTime end, List uris, boolean unique) { + if (unique) { + if (uris == null || uris.isEmpty()) { + return Mapper.toViewStatsDtoList(repository.findUniqueIpStatsBetweenDates(start, end)); + } else { + return Mapper.toViewStatsDtoList(repository.findUniqueIpStatsBetweenDatesAndUris(start, end, uris)); + } + } else { + if (uris == null || uris.isEmpty()) { + return Mapper.toViewStatsDtoList(repository.findStatsBetweenDates(start, end)); + } else { + return Mapper.toViewStatsDtoList(repository.findStatsBetweenDatesAndUris(start, end, uris)); + } + } + } +} diff --git a/stats/stats-server/src/main/resources/application.properties b/stats/stats-server/src/main/resources/application.properties new file mode 100644 index 00000000..70a6d32c --- /dev/null +++ b/stats/stats-server/src/main/resources/application.properties @@ -0,0 +1,23 @@ +server.port=9090 + +spring.datasource.url=jdbc:postgresql://localhost:5432/exploreWithMe +spring.datasource.username=postgres +spring.datasource.password=12345 +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.sql.init.mode=always +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +logging.level.org.springframework.orm.jpa=INFO +logging.level.org.springframework.transaction=INFO +logging.level.org.springframework.transaction.interceptor=TRACE +logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG + +#--- +spring.config.activate.on-profile=ci,test +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:exploreWithMe +spring.datasource.username=test +spring.datasource.password=test \ No newline at end of file diff --git a/stats/stats-server/src/main/resources/schema.sql b/stats/stats-server/src/main/resources/schema.sql new file mode 100644 index 00000000..b6fda3c7 --- /dev/null +++ b/stats/stats-server/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS public.endpoint_hits CASCADE; +CREATE TABLE IF NOT EXISTS endpoint_hits ( + id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + app VARCHAR(30) NOT NULL, + uri VARCHAR(255) NOT NULL, + ip VARCHAR(15) NOT NULL, + time_stamp TIMESTAMP WITHOUT TIME ZONE NOT NULL +); \ No newline at end of file