diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 8ab19ff..5a7784a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -18,4 +18,17 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index db3d380..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index e9d201e..9d230b5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -11,4 +11,5 @@ rootProject.name = 'VegaHRM.Backend'
include 'vega-hrm-auth'
include 'vega-hrm-core'
include 'vega-hrm-report'
+include 'vega-hrm-chat'
diff --git a/vega-hrm-chat/build.gradle b/vega-hrm-chat/build.gradle
new file mode 100644
index 0000000..4a90552
--- /dev/null
+++ b/vega-hrm-chat/build.gradle
@@ -0,0 +1,61 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.4.0'
+ id 'io.spring.dependency-management' version '1.1.6'
+}
+
+group = 'com.vega.hrm.chat'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.4.0'
+ implementation 'org.springframework.boot:spring-boot-starter-web:3.4.0'
+ implementation 'org.projectlombok:lombok:1.18.38'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
+ implementation('org.springframework.boot:spring-boot-starter:3.4.0') {
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
+ }
+ implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
+ implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
+ // Kafka
+ implementation 'org.springframework.kafka:spring-kafka'
+ // Redis
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ annotationProcessor 'org.projectlombok:lombok:1.18.38'
+ implementation project(":vega-hrm-core")
+ implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
+ implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.0"))
+
+}
+
+configurations {
+ all {
+ // Loại bỏ Logback
+ exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
+
+ // Loại bỏ logging mặc định
+ exclude group: 'ch.qos.logback', module: 'logback-classic'
+
+ // Loại bỏ các thư viện Guava/Google Collections cũ
+ exclude group: 'com.google.guava', module: 'guava-jdk5'
+ exclude group: 'com.google.collections', module: 'google-collections'
+
+ resolutionStrategy {
+ // Bắt buộc dùng Guava mới nhất
+ force 'com.google.guava:guava:32.1.3-jre'
+ force 'com.google.api-client:google-api-client:1.34.1'
+ force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ force 'com.google.http-client:google-http-client-jackson2:1.43.3'
+ }
+ }
+}
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/VegaHrmChatApplication.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/VegaHrmChatApplication.java
new file mode 100644
index 0000000..4155b46
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/VegaHrmChatApplication.java
@@ -0,0 +1,14 @@
+package com.vega.hrm.chat;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class VegaHrmChatApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(VegaHrmChatApplication.class, args);
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/KafkaConfig.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/KafkaConfig.java
new file mode 100644
index 0000000..9fcc424
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/KafkaConfig.java
@@ -0,0 +1,53 @@
+package com.vega.hrm.chat.config;
+
+import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.TopicBuilder;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class KafkaConfig {
+
+ @Value("${chat.kafka.bootstrap-servers:localhost:9092}")
+ private String bootstrapServers;
+
+ @Value("${chat.kafka.topic.chat:vega-hrm-chat}")
+ private String chatTopicName;
+
+ @Bean
+ public NewTopic chatTopic() {
+ return TopicBuilder.name(chatTopicName)
+ .partitions(6)
+ .replicas(1)
+ .compact() // chat message log có thể dùng compact, tùy nhu cầu
+ .build();
+ }
+
+ @Bean
+ public ProducerFactory producerFactory() {
+ Map configProps = new HashMap<>();
+ configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ configProps.put(ProducerConfig.ACKS_CONFIG, "all");
+ configProps.put(ProducerConfig.LINGER_MS_CONFIG, 5);
+ configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 32_768);
+ return new DefaultKafkaProducerFactory<>(configProps);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/RedisConfig.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/RedisConfig.java
new file mode 100644
index 0000000..eb5f043
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/RedisConfig.java
@@ -0,0 +1,33 @@
+package com.vega.hrm.chat.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public RedisConnectionFactory redisConnectionFactory() {
+ // Có thể chuyển sang cấu hình qua application.properties (host, port, password)
+ return new LettuceConnectionFactory();
+ }
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
+ template.afterPropertiesSet();
+ return template;
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/WebSocketConfig.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/WebSocketConfig.java
new file mode 100644
index 0000000..f46cc0e
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/config/WebSocketConfig.java
@@ -0,0 +1,29 @@
+package com.vega.hrm.chat.config;
+
+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) {
+ // Broker đơn giản in-memory cho demo; có thể scale bằng Kafka/RabbitMQ sau
+ config.enableSimpleBroker("/topic", "/queue");
+ config.setApplicationDestinationPrefixes("/app");
+ }
+
+ @Override
+ public void registerStompEndpoints(StompEndpointRegistry registry) {
+ registry.addEndpoint("/ws/chat")
+ .setAllowedOriginPatterns("*");
+ // Nếu cần SockJS:
+ // .withSockJS();
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatController.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatController.java
new file mode 100644
index 0000000..ab39378
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatController.java
@@ -0,0 +1,30 @@
+package com.vega.hrm.chat.controller;
+
+import com.vega.hrm.chat.model.ChatMessage;
+import com.vega.hrm.chat.service.ChatService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/chat")
+public class ChatController {
+
+ private final ChatService chatService;
+
+ public ChatController(ChatService chatService) {
+ this.chatService = chatService;
+ }
+
+ /**
+ * API REST chỉ dùng để tạo box chat / group lần đầu,
+ * gửi message realtime sẽ đi qua WebSocket.
+ */
+ @PostMapping("/rooms")
+ public ResponseEntity createRoom(@RequestParam String roomId) {
+ // TODO: sau này có thể lưu meta vào DB (tên phòng, thành viên, loại phòng...)
+ // Hiện tại chỉ cần đảm bảo roomId hợp lệ và trả về cho FE dùng.
+ return ResponseEntity.ok(roomId);
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatSocketController.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatSocketController.java
new file mode 100644
index 0000000..3f5ef31
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/controller/ChatSocketController.java
@@ -0,0 +1,33 @@
+package com.vega.hrm.chat.controller;
+
+import com.vega.hrm.chat.model.ChatMessage;
+import com.vega.hrm.chat.service.ChatService;
+import org.springframework.messaging.handler.annotation.DestinationVariable;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class ChatSocketController {
+
+ private final ChatService chatService;
+
+ public ChatSocketController(ChatService chatService) {
+ this.chatService = chatService;
+ }
+
+ /**
+ * Client gửi message vào: /app/chat/{roomId}
+ * Server publish ra topic: /topic/chat/{roomId}
+ */
+ @MessageMapping("/chat/{roomId}")
+ @SendTo("/topic/chat/{roomId}")
+ public ChatMessage sendToRoom(@DestinationVariable String roomId,
+ @Payload ChatMessage message) {
+ message.setRoomId(roomId);
+ return chatService.sendMessage(message);
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/model/ChatMessage.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/model/ChatMessage.java
new file mode 100644
index 0000000..b6cf5fa
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/model/ChatMessage.java
@@ -0,0 +1,63 @@
+package com.vega.hrm.chat.model;
+
+import java.time.Instant;
+
+public class ChatMessage {
+
+ private String id;
+ private String roomId; // ví dụ: theo phòng ban, project, conversation id
+ private String senderId;
+ private String receiverId; // optional, nếu là 1-1 chat
+ private String content;
+ private Instant sentAt;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getRoomId() {
+ return roomId;
+ }
+
+ public void setRoomId(String roomId) {
+ this.roomId = roomId;
+ }
+
+ public String getSenderId() {
+ return senderId;
+ }
+
+ public void setSenderId(String senderId) {
+ this.senderId = senderId;
+ }
+
+ public String getReceiverId() {
+ return receiverId;
+ }
+
+ public void setReceiverId(String receiverId) {
+ this.receiverId = receiverId;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public Instant getSentAt() {
+ return sentAt;
+ }
+
+ public void setSentAt(Instant sentAt) {
+ this.sentAt = sentAt;
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/java/com/vega/hrm/chat/service/ChatService.java b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/service/ChatService.java
new file mode 100644
index 0000000..3dc5d63
--- /dev/null
+++ b/vega-hrm-chat/src/main/java/com/vega/hrm/chat/service/ChatService.java
@@ -0,0 +1,62 @@
+package com.vega.hrm.chat.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.vega.hrm.chat.model.ChatMessage;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class ChatService {
+
+ private final KafkaTemplate kafkaTemplate;
+ private final RedisTemplate redisTemplate;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Value("${chat.kafka.topic.chat:vega-hrm-chat}")
+ private String chatTopicName;
+
+ @Value("${chat.redis.recent-ttl-minutes:1440}")
+ private long recentMessagesTtlMinutes;
+
+ public ChatService(KafkaTemplate kafkaTemplate,
+ RedisTemplate redisTemplate) {
+ this.kafkaTemplate = kafkaTemplate;
+ this.redisTemplate = redisTemplate;
+ }
+
+ public ChatMessage sendMessage(ChatMessage input) {
+ ChatMessage message = new ChatMessage();
+ message.setId(UUID.randomUUID().toString());
+ message.setRoomId(input.getRoomId());
+ message.setSenderId(input.getSenderId());
+ message.setReceiverId(input.getReceiverId());
+ message.setContent(input.getContent());
+ message.setSentAt(Instant.now());
+
+ // 1. Gửi vào Kafka để các service khác consume (log, phân tích, notify...)
+ try {
+ String payload = objectMapper.writeValueAsString(message);
+ kafkaTemplate.send(chatTopicName, message.getRoomId(), payload);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Cannot serialize chat message", e);
+ }
+
+ // 2. Lưu recent messages trong Redis để load nhanh lịch sử gần đây
+ String redisKey = "chat:room:" + message.getRoomId() + ":recent";
+ redisTemplate.opsForList().rightPush(redisKey, message);
+ redisTemplate.expire(redisKey, recentMessagesTtlMinutes, TimeUnit.MINUTES);
+
+ // 3. Có thể update trạng thái “unread”/“last_seen” ở đây (tương lai)
+
+ return message;
+ }
+}
+
+
diff --git a/vega-hrm-chat/src/main/resources/application.properties b/vega-hrm-chat/src/main/resources/application.properties
new file mode 100644
index 0000000..6cd8009
--- /dev/null
+++ b/vega-hrm-chat/src/main/resources/application.properties
@@ -0,0 +1,17 @@
+spring.application.name=vega-hrm-chat
+
+# HTTP
+server.port=8093
+
+# Kafka
+chat.kafka.bootstrap-servers=localhost:9092
+chat.kafka.topic.chat=vega-hrm-chat
+
+# Redis (tối ưu: chuyển sang config riêng theo môi trường)
+spring.redis.host=localhost
+spring.redis.port=6379
+
+# TTL lịch sử chat gần nhất trong Redis (phút)
+chat.redis.recent-ttl-minutes=1440
+
+