Skip to content

Commit 41fda16

Browse files
Added changes
1 parent 994c388 commit 41fda16

38 files changed

+815
-125
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,43 @@ jobs:
3535
sleep 2
3636
done
3737
38+
# Можно аналогично добавить wait for kafka/mailhog если нужно
39+
3840
- name: Set up OpenJDK 21
3941
uses: actions/setup-java@v4
4042
with:
4143
distribution: 'temurin'
4244
java-version: '21'
4345

44-
- name: Build and run tests
46+
- name: Build and run tests (user-service)
47+
run: mvn verify
48+
working-directory: ./user-service
49+
50+
- name: Build and run tests (notification-service)
4551
run: mvn verify
52+
working-directory: ./notification-service
4653

47-
- name: Upload JaCoCo report
54+
- name: Upload JaCoCo report (user-service)
4855
uses: actions/upload-artifact@v4
4956
with:
50-
name: jacoco-report
51-
path: target/site/jacoco
57+
name: jacoco-report-user-service
58+
path: user-service/target/site/jacoco
5259

53-
- name: Upload JaCoCo coverage to Qlty Cloud
54-
uses: qltysh/qlty-action/coverage@v1
60+
- name: Upload JaCoCo report (notification-service)
61+
uses: actions/upload-artifact@v4
5562
with:
56-
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
57-
files: target/site/jacoco/jacoco.xml
63+
name: jacoco-report-notification-service
64+
path: notification-service/target/site/jacoco
65+
5866

67+
- name: Run Checkstyle (user-service)
68+
run: mvn checkstyle:check
69+
working-directory: ./user-service
5970

60-
- name: Run Checkstyle
71+
- name: Run Checkstyle (notification-service)
6172
run: mvn checkstyle:check
73+
working-directory: ./notification-service
6274

6375
- name: Tear down Docker Compose
6476
if: always()
65-
run: docker compose down
77+
run: docker compose down

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
[![Maintainability](https://qlty.sh/badges/a56c7491-b9be-4239-964a-541250c083e3/maintainability.svg)](https://qlty.sh/gh/irinakomarchenko/projects/user-service-spring)
44

5-
# User Service Spring
5+
# Notification-service
66

7-
**User Service Spring** — REST API-сервис на Spring Boot для управления пользователями (CRUD).
7+
**Notification-service** — REST API-сервис на Spring Boot для управления пользователями (CRUD).
8+
Сервис отправляет Kafka-события о создании и удалении пользователей. Эти события могут быть обработаны, например, микросервисом notification-service, который отправляет email-уведомления.
89

910
---
1011

@@ -14,7 +15,9 @@
1415
- Spring Boot
1516
- Spring Web (REST API)
1617
- Spring Data JPA (встроенный Hibernate)
18+
- Spring Kafka (Kafka Producer)
1719
- PostgreSQL (через Docker Compose)
20+
- Apache Kafka (интеграция через Spring Kafka)
1821
- SLF4J + Logback (логирование)
1922
- JUnit 5 + MockMvc (тестирование контроллеров и API)
2023
- Maven (сборка и зависимости)
@@ -27,10 +30,10 @@
2730
### 1. Клонировать репозиторий
2831

2932
```sh
30-
git clone https://github.com/ТВОЙ_ЛОГИН/user-service-hibernate.git
31-
cd user-service-hibernate
33+
git clone https://github.com/ТВОЙ_ЛОГИН/user-service-spring-Apache-Kafka.git
34+
cd user-service-spring-Apache Kafka
3235
```
33-
### 2 Запустить базу данных PostgreSQL
36+
### Запустить PostgreSQL и Kafka (Docker Compose)
3437
```sh
3538
docker compose up -d
3639
```

docker-compose.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,37 @@ services:
44
image: postgres:14.1-alpine
55
restart: always
66
environment:
7-
- POSTGRES_USER=postgres
8-
- POSTGRES_PASSWORD=postgres
7+
POSTGRES_USER: postgres
8+
POSTGRES_PASSWORD: postgres
99
ports:
1010
- '5432:5432'
1111
volumes:
1212
- db:/var/lib/postgresql/data
13+
14+
zookeeper:
15+
image: confluentinc/cp-zookeeper:7.5.0
16+
environment:
17+
ZOOKEEPER_CLIENT_PORT: 2181
18+
ZOOKEEPER_TICK_TIME: 2000
19+
20+
kafka:
21+
image: confluentinc/cp-kafka:7.5.0
22+
depends_on:
23+
- zookeeper
24+
ports:
25+
- '9092:9092'
26+
environment:
27+
KAFKA_BROKER_ID: 1
28+
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
29+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
30+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
31+
32+
mailhog:
33+
image: mailhog/mailhog
34+
ports:
35+
- '1025:1025' # SMTP
36+
- '8025:8025' # Web UI
37+
1338
volumes:
1439
db:
1540
driver: local

notification-service/pom.xml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0"
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
4+
https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>myuserservice</groupId>
9+
<artifactId>user-service-spring-Apache-Kafka</artifactId>
10+
<version>1.0-SNAPSHOT</version>
11+
<relativePath>../pom.xml</relativePath>
12+
</parent>
13+
14+
<artifactId>notification-service</artifactId>
15+
<packaging>jar</packaging>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>org.springframework.boot</groupId>
20+
<artifactId>spring-boot-starter-web</artifactId>
21+
</dependency>
22+
<dependency>
23+
<groupId>org.springframework.kafka</groupId>
24+
<artifactId>spring-kafka</artifactId>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-starter-mail</artifactId>
29+
</dependency>
30+
<dependency>
31+
<groupId>com.sun.mail</groupId>
32+
<artifactId>jakarta.mail</artifactId>
33+
<version>2.0.1</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.springframework.boot</groupId>
37+
<artifactId>spring-boot-starter-validation</artifactId>
38+
</dependency>
39+
<dependency>
40+
<groupId>com.fasterxml.jackson.core</groupId>
41+
<artifactId>jackson-databind</artifactId>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.projectlombok</groupId>
45+
<artifactId>lombok</artifactId>
46+
<scope>provided</scope>
47+
</dependency>
48+
49+
50+
<dependency>
51+
<groupId>org.springframework.boot</groupId>
52+
<artifactId>spring-boot-starter-test</artifactId>
53+
<scope>test</scope>
54+
</dependency>
55+
<dependency>
56+
<groupId>org.springframework.kafka</groupId>
57+
<artifactId>spring-kafka-test</artifactId>
58+
<scope>test</scope>
59+
</dependency>
60+
<dependency>
61+
<groupId>org.testcontainers</groupId>
62+
<artifactId>junit-jupiter</artifactId>
63+
<scope>test</scope>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.testcontainers</groupId>
67+
<artifactId>kafka</artifactId>
68+
<scope>test</scope>
69+
</dependency>
70+
</dependencies>
71+
72+
<build>
73+
<plugins>
74+
<plugin>
75+
<groupId>org.springframework.boot</groupId>
76+
<artifactId>spring-boot-maven-plugin</artifactId>
77+
<configuration>
78+
<mainClass>notificationservice.NotificationServiceApplication</mainClass>
79+
</configuration>
80+
</plugin>
81+
82+
<plugin>
83+
<groupId>org.apache.maven.plugins</groupId>
84+
<artifactId>maven-compiler-plugin</artifactId>
85+
<version>3.13.0</version>
86+
<configuration>
87+
<source>21</source>
88+
<target>21</target>
89+
</configuration>
90+
</plugin>
91+
</plugins>
92+
</build>
93+
</project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package notificationservice;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
7+
@SpringBootApplication
8+
@EnableAutoConfiguration
9+
public class NotificationServiceApplication {
10+
11+
public static void main(String[] args) {
12+
13+
SpringApplication.run(NotificationServiceApplication.class, args);
14+
}
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package notificationservice.advice;
2+
3+
import jakarta.persistence.EntityNotFoundException;
4+
import org.springframework.dao.DataIntegrityViolationException;
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.web.bind.MethodArgumentNotValidException;
8+
import org.springframework.web.bind.annotation.ExceptionHandler;
9+
import org.springframework.web.bind.annotation.RestControllerAdvice;
10+
11+
import java.util.stream.Collectors;
12+
13+
14+
@RestControllerAdvice
15+
public class GlobalExceptionHandler {
16+
17+
@ExceptionHandler(EntityNotFoundException.class)
18+
public ResponseEntity<String> handleNotFound(EntityNotFoundException ex) {
19+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
20+
}
21+
22+
@ExceptionHandler(DataIntegrityViolationException.class)
23+
public ResponseEntity<String> handleDuplicate(DataIntegrityViolationException ex) {
24+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Email already exists");
25+
}
26+
27+
@ExceptionHandler(MethodArgumentNotValidException.class)
28+
public ResponseEntity<String> handleValidation(MethodArgumentNotValidException ex) {
29+
String message = ex.getBindingResult().getFieldErrors().stream()
30+
.map(err -> err.getField() + ": " + err.getDefaultMessage())
31+
.collect(Collectors.joining(", "));
32+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Validation error: " + message);
33+
}
34+
35+
@ExceptionHandler(Exception.class)
36+
public ResponseEntity<String> handleGeneric(Exception ex) {
37+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
38+
.body("Unexpected error occurred");
39+
}
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package notificationservice.controller;
2+
3+
4+
import lombok.RequiredArgsConstructor;
5+
import notificationservice.dto.UserEventNotificDto;
6+
import notificationservice.service.EmailService;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequestMapping("/api/emails")
16+
@RequiredArgsConstructor
17+
public class EmailController {
18+
private final EmailService emailService;
19+
private static final Logger log = LoggerFactory.getLogger(EmailController.class);
20+
21+
@PostMapping("/send")
22+
public void sendEmail(@RequestBody UserEventNotificDto event) {
23+
log.info("Запрос на отправку письма через API: operation={}, email={}", event.getOperation(), event.getEmail());
24+
if ("CREATE".equals(event.getOperation())) {
25+
emailService.send(event.getEmail(), "Ваш аккаунт создан", "Здравствуйте! Ваш аккаунт на сайте ваш сайт был успешно создан.");
26+
} else if ("DELETE".equals(event.getOperation())) {
27+
emailService.send(event.getEmail(), "Ваш аккаунт удалён", "Здравствуйте! Ваш аккаунт был удалён.");
28+
} else {
29+
log.warn("Неизвестная операция: {}", event.getOperation());
30+
}
31+
32+
}
33+
34+
35+
36+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package notificationservice.dto;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class UserEventNotificDto {
7+
private String operation; // "CREATE" или "DELETE"
8+
private String email;
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package notificationservice.kafka;
2+
3+
4+
import lombok.RequiredArgsConstructor;
5+
import notificationservice.dto.UserEventNotificDto;
6+
import notificationservice.service.EmailService;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.kafka.annotation.KafkaListener;
10+
import org.springframework.stereotype.Service;
11+
12+
@Service
13+
@RequiredArgsConstructor
14+
public class UserEventConsumer {
15+
private static final Logger log = LoggerFactory.getLogger(UserEventConsumer.class);
16+
17+
private final EmailService emailService;
18+
19+
@KafkaListener(topics = "user-events", groupId = "notification-service")
20+
public void listen(UserEventNotificDto event) {
21+
log.info("Получено событие из Kafka: operation={}, email={}", event.getOperation(), event.getEmail());
22+
try {
23+
if ("CREATE".equals(event.getOperation())) {
24+
emailService.send(event.getEmail(), "Ваш аккаунт создан", "Здравствуйте! Ваш аккаунт на сайте ваш сайт был успешно создан.");
25+
} else if ("DELETE".equals(event.getOperation())) {
26+
emailService.send(event.getEmail(), "Ваш аккаунт удалён", "Здравствуйте! Ваш аккаунт был удалён.");
27+
} else {
28+
log.warn("Неизвестная операция: {}", event.getOperation());
29+
}
30+
} catch (Exception e) {
31+
log.error("Ошибка обработки события из Kafka: {}", e.getMessage(), e);
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)