feat : chat
This commit is contained in:
parent
3c055be676
commit
593d3d6a19
|
|
@ -18,4 +18,17 @@
|
|||
<module name="vega-hrm-core.test" target="21" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="VegaHRM.Backend.vega-hrm-auth" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-auth.main" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-auth.test" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-core" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-core.main" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-core.test" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-report" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-report.main" options="-parameters" />
|
||||
<module name="VegaHRM.Backend.vega-hrm-report.test" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/vega-hrm-auth/VegaHRM.Backend.vega-hrm-auth.main.iml" filepath="$PROJECT_DIR$/.idea/modules/vega-hrm-auth/VegaHRM.Backend.vega-hrm-auth.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/vega-hrm-core/VegaHRM.Backend.vega-hrm-core.main.iml" filepath="$PROJECT_DIR$/.idea/modules/vega-hrm-core/VegaHRM.Backend.vega-hrm-core.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/vega-hrm-report/VegaHRM.Backend.vega-hrm-report.main.iml" filepath="$PROJECT_DIR$/.idea/modules/vega-hrm-report/VegaHRM.Backend.vega-hrm-report.main.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -11,4 +11,5 @@ rootProject.name = 'VegaHRM.Backend'
|
|||
include 'vega-hrm-auth'
|
||||
include 'vega-hrm-core'
|
||||
include 'vega-hrm-report'
|
||||
include 'vega-hrm-chat'
|
||||
|
||||
|
|
|
|||
61
vega-hrm-chat/build.gradle
Normal file
61
vega-hrm-chat/build.gradle
Normal file
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<String, String> producerFactory() {
|
||||
Map<String, Object> 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<String, String> kafkaTemplate() {
|
||||
return new KafkaTemplate<>(producerFactory());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<String, String> kafkaTemplate;
|
||||
private final RedisTemplate<String, Object> 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<String, String> kafkaTemplate,
|
||||
RedisTemplate<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
vega-hrm-chat/src/main/resources/application.properties
Normal file
17
vega-hrm-chat/src/main/resources/application.properties
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user