bổ sung thêm chức năng
This commit is contained in:
@@ -9,4 +9,4 @@ RUN mvn clean package -DskipTests
|
||||
FROM eclipse-temurin:21-jre
|
||||
WORKDIR /app
|
||||
COPY --from=build /build/target/*.jar app.jar
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.data.mongodb.config.EnableMongoAuditing;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableMongoAuditing
|
||||
@org.springframework.scheduling.annotation.EnableScheduling
|
||||
public class UserServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserServiceApplication.class, args);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.dken.userservice.client;
|
||||
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.dken.userservice.dto.response.UserApplyGiftCodeResponse;
|
||||
import com.dken.userservice.dto.request.GiftCodeValidateRequest;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class GiftCodeClient {
|
||||
@@ -15,15 +15,27 @@ public class GiftCodeClient {
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
public UserApplyGiftCodeResponse applyGiftCode(
|
||||
String userId,
|
||||
String giftCode) {
|
||||
|
||||
return restClient
|
||||
.post()
|
||||
.uri("/gift-codes/apply")
|
||||
.body(new GiftCodeValidateRequest(userId, giftCode))
|
||||
public Integer reserve(String transactionId, String userId, String code) {
|
||||
return restClient.post()
|
||||
.uri("/gift-codes/reserve")
|
||||
.body(Map.of("transactionId", transactionId, "userId", userId, "code", code))
|
||||
.retrieve()
|
||||
.body(UserApplyGiftCodeResponse.class);
|
||||
.body(Integer.class);
|
||||
}
|
||||
|
||||
public void confirm(String transactionId) {
|
||||
restClient.post()
|
||||
.uri("/gift-codes/confirm")
|
||||
.body(Map.of("transactionId", transactionId))
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
public void cancel(String transactionId) {
|
||||
restClient.post()
|
||||
.uri("/gift-codes/cancel")
|
||||
.body(Map.of("transactionId", transactionId))
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ public class GiftCodeClientConfig {
|
||||
|
||||
@Bean("giftCodeRestClient")
|
||||
public RestClient giftCodeRestClient(
|
||||
@Value("${giftcode.service.url}") String baseUrl) {
|
||||
@Value("${giftcode.service.url}") String baseUrl,
|
||||
@Value("${internal.api-key}") String apiKey) {
|
||||
return RestClient.builder()
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeader("X-Internal-Key", apiKey)
|
||||
.requestInterceptor((request, body, execution) -> {
|
||||
return execution.execute(request, body);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.dken.userservice.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
|
||||
@Configuration
|
||||
@Profile("!dev") // Không active khi profile là "dev"
|
||||
public class MongoConfig extends AbstractMongoClientConfiguration {
|
||||
|
||||
@Value("${spring.data.mongodb.uri:mongodb://mongodb:27017/userdb}")
|
||||
private String mongoUri;
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
ConnectionString connectionString = new ConnectionString(mongoUri);
|
||||
return connectionString.getDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MongoClient mongoClient() {
|
||||
ConnectionString connectionString = new ConnectionString(mongoUri);
|
||||
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
|
||||
.applyConnectionString(connectionString)
|
||||
.build();
|
||||
return MongoClients.create(mongoClientSettings);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dken.userservice.dto.request;
|
||||
|
||||
public record GiftCodeValidateRequest(
|
||||
String userId,
|
||||
String giftCode) {
|
||||
String userId,
|
||||
String giftCode,
|
||||
String requestId) {
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
package com.dken.userservice.dto.response;
|
||||
|
||||
public class UserApplyGiftCodeResponse {
|
||||
private String status;
|
||||
private String message;
|
||||
private String description;
|
||||
private String status;
|
||||
private Integer amount;
|
||||
private String requestId;
|
||||
|
||||
public UserApplyGiftCodeResponse(String status, String message, String description) {
|
||||
this.status = status;
|
||||
public UserApplyGiftCodeResponse() {
|
||||
}
|
||||
|
||||
public UserApplyGiftCodeResponse(String message, String status, Integer amount, String requestId) {
|
||||
this.message = message;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
this.amount = amount;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
@@ -27,11 +24,27 @@ public class UserApplyGiftCodeResponse {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Integer getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Integer amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
public void setRequestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public class UserResponse {
|
||||
private String name;
|
||||
private String email;
|
||||
private String phone;
|
||||
private Long balance = 0L;
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
@@ -36,15 +37,17 @@ public class UserResponse {
|
||||
user.getName(),
|
||||
user.getEmail(),
|
||||
user.getPhone(),
|
||||
user.getSex() != null ? user.getSex().name() : null);
|
||||
user.getSex() != null ? user.getSex().name() : null,
|
||||
user.getBalance());
|
||||
}
|
||||
|
||||
public UserResponse(String id, String name, String email, String phone, String sex) {
|
||||
public UserResponse(String id, String name, String email, String phone, String sex, Long balance) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.phone = phone;
|
||||
this.sex = sex;
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
@@ -71,4 +74,12 @@ public class UserResponse {
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Long getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public void setBalance(Long balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.dken.userservice.exception;
|
||||
|
||||
public class GiftCodeReservationException extends RuntimeException {
|
||||
public GiftCodeReservationException(String message) {
|
||||
super("Failed to reserve gift code: " + message);
|
||||
}
|
||||
}
|
||||
@@ -27,4 +27,22 @@ public class GlobalExceptionHandler {
|
||||
public Map<String, String> handleNotFound(UserNotFoundException ex) {
|
||||
return Map.of("message", ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(GiftCodeReservationException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Map<String, String> handleGiftCodeReservation(GiftCodeReservationException ex) {
|
||||
return Map.of("message", ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(LocalProcessingException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Map<String, String> handleLocalProcessing(LocalProcessingException ex) {
|
||||
return Map.of("message", ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Map<String, String> handleRuntimeException(RuntimeException ex) {
|
||||
return Map.of("message", "An unexpected error occurred: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.dken.userservice.exception;
|
||||
|
||||
public class LocalProcessingException extends RuntimeException {
|
||||
public LocalProcessingException(String message) {
|
||||
super("Local transaction failed: " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.dken.userservice.model;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import java.time.Instant;
|
||||
|
||||
@Document(collection = "redeem_logs")
|
||||
public class RedeemLog {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Indexed(unique = true)
|
||||
private String transactionId;
|
||||
|
||||
@Indexed
|
||||
private String userId;
|
||||
|
||||
private String code;
|
||||
private Integer amount;
|
||||
private RedeemLogStatus status;
|
||||
private Instant createdAt;
|
||||
private Instant lastRetryAt;
|
||||
|
||||
public RedeemLog() {
|
||||
}
|
||||
|
||||
public RedeemLog(String transactionId, String userId, String code, Integer amount, RedeemLogStatus status) {
|
||||
this.transactionId = transactionId;
|
||||
this.userId = userId;
|
||||
this.code = code;
|
||||
this.amount = amount;
|
||||
this.status = status;
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public void setTransactionId(String transactionId) {
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public Integer getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Integer amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public RedeemLogStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(RedeemLogStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getLastRetryAt() {
|
||||
return lastRetryAt;
|
||||
}
|
||||
|
||||
public void setLastRetryAt(Instant lastRetryAt) {
|
||||
this.lastRetryAt = lastRetryAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.dken.userservice.model;
|
||||
|
||||
public enum RedeemLogStatus {
|
||||
SUCCESS,
|
||||
PENDING_CONFIRM,
|
||||
FAILED
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class User {
|
||||
private String email;
|
||||
private String phone;
|
||||
private UserSex sex;
|
||||
private Long balance = 0L;
|
||||
|
||||
private UserStatus status;
|
||||
|
||||
@@ -102,6 +103,14 @@ public class User {
|
||||
this.sex = sex;
|
||||
}
|
||||
|
||||
public Long getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public void setBalance(Long balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.dken.userservice.repository;
|
||||
|
||||
import com.dken.userservice.model.RedeemLog;
|
||||
import com.dken.userservice.model.RedeemLogStatus;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import java.util.List;
|
||||
|
||||
public interface RedeemLogRepository extends MongoRepository<RedeemLog, String> {
|
||||
List<RedeemLog> findByStatus(RedeemLogStatus status);
|
||||
|
||||
RedeemLog findByTransactionId(String transactionId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.dken.userservice.scheduler;
|
||||
|
||||
import com.dken.userservice.client.GiftCodeClient;
|
||||
import com.dken.userservice.model.RedeemLog;
|
||||
import com.dken.userservice.model.RedeemLogStatus;
|
||||
import com.dken.userservice.repository.RedeemLogRepository;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class RetryConfirmScheduler {
|
||||
|
||||
private final RedeemLogRepository redeemLogRepository;
|
||||
private final GiftCodeClient giftCodeClient;
|
||||
|
||||
public RetryConfirmScheduler(RedeemLogRepository redeemLogRepository, GiftCodeClient giftCodeClient) {
|
||||
this.redeemLogRepository = redeemLogRepository;
|
||||
this.giftCodeClient = giftCodeClient;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 60000)
|
||||
public void retryConfirms() {
|
||||
List<RedeemLog> pendingLogs = redeemLogRepository.findByStatus(RedeemLogStatus.PENDING_CONFIRM);
|
||||
for (RedeemLog log : pendingLogs) {
|
||||
try {
|
||||
giftCodeClient.confirm(log.getTransactionId());
|
||||
log.setStatus(RedeemLogStatus.SUCCESS);
|
||||
redeemLogRepository.save(log);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Retry confirm failed for " + log.getTransactionId() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,25 @@ import com.dken.userservice.dto.request.CreateUserRequest;
|
||||
import com.dken.userservice.dto.request.UserApplyGiftCodeRequest;
|
||||
import com.dken.userservice.dto.response.UserApplyGiftCodeResponse;
|
||||
import com.dken.userservice.dto.response.UserResponse;
|
||||
import com.dken.userservice.model.RedeemLog;
|
||||
import com.dken.userservice.model.RedeemLogStatus;
|
||||
import com.dken.userservice.model.User;
|
||||
import com.dken.userservice.model.UserSex;
|
||||
import com.dken.userservice.repository.RedeemLogRepository;
|
||||
import com.dken.userservice.repository.UserRepository;
|
||||
import com.dken.userservice.exception.InvalidUserIdException;
|
||||
import com.dken.userservice.exception.GiftCodeReservationException;
|
||||
import com.dken.userservice.exception.LocalProcessingException;
|
||||
import com.dken.userservice.exception.UserNotFoundException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
@Service
|
||||
@@ -23,18 +31,18 @@ public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final GiftCodeClient giftCodeClient;
|
||||
private final RedeemLogRepository redeemLogRepository;
|
||||
|
||||
public UserService(UserRepository userRepository, GiftCodeClient giftCodeClient) {
|
||||
public UserService(UserRepository userRepository, GiftCodeClient giftCodeClient,
|
||||
RedeemLogRepository redeemLogRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.giftCodeClient = giftCodeClient;
|
||||
this.redeemLogRepository = redeemLogRepository;
|
||||
}
|
||||
|
||||
public Page<UserResponse> getAllUsers(int page, int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
|
||||
return userRepository.findAll(pageable)
|
||||
.map(UserResponse::from);
|
||||
return userRepository.findAll(pageable).map(UserResponse::from);
|
||||
}
|
||||
|
||||
public User create(String name, String email, String password, String phone, String sex) {
|
||||
@@ -56,12 +64,64 @@ public class UserService {
|
||||
if (!ObjectId.isValid(id)) {
|
||||
throw new InvalidUserIdException(id);
|
||||
}
|
||||
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
|
||||
User user = userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
|
||||
return UserResponse.from(user);
|
||||
}
|
||||
|
||||
public UserApplyGiftCodeResponse applyGiftCode(UserApplyGiftCodeRequest req) {
|
||||
User user = userRepository.findById(req.getUserId()).orElseThrow(() -> new RuntimeException("User not found"));
|
||||
return giftCodeClient.applyGiftCode(req.getUserId(), req.getGiftCode());
|
||||
String txId = UUID.randomUUID().toString();
|
||||
|
||||
// 1. Reserve Remotely (Saga Start)
|
||||
Integer amount;
|
||||
try {
|
||||
amount = giftCodeClient.reserve(txId, req.getUserId(), req.getGiftCode());
|
||||
} catch (Exception e) {
|
||||
throw new GiftCodeReservationException(e.getMessage());
|
||||
}
|
||||
|
||||
// 2. Local Transaction
|
||||
try {
|
||||
processLocalRedeem(txId, req.getUserId(), req.getGiftCode(), amount);
|
||||
} catch (Exception e) {
|
||||
// Compensate Remote
|
||||
try {
|
||||
giftCodeClient.cancel(txId);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
throw new LocalProcessingException(e.getMessage());
|
||||
}
|
||||
|
||||
// 3. Confirm Remote
|
||||
try {
|
||||
giftCodeClient.confirm(txId);
|
||||
updateLogStatus(txId, RedeemLogStatus.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
// Confirm failed (Network mainly), Job will retry
|
||||
// Log status is still PENDING_CONFIRM
|
||||
System.err.println("Confirm failed for " + txId + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
return new UserApplyGiftCodeResponse("Gift code applied successfully", "success", amount, txId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void processLocalRedeem(String txId, String userId, String code, Integer amount) {
|
||||
User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
|
||||
if (user.getBalance() == null)
|
||||
user.setBalance(0L);
|
||||
user.setBalance(user.getBalance() + amount);
|
||||
userRepository.save(user);
|
||||
|
||||
RedeemLog log = new RedeemLog(txId, userId, code, amount, RedeemLogStatus.PENDING_CONFIRM);
|
||||
redeemLogRepository.save(log);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateLogStatus(String txId, RedeemLogStatus status) {
|
||||
RedeemLog log = redeemLogRepository.findByTransactionId(txId);
|
||||
if (log != null) {
|
||||
log.setStatus(status);
|
||||
redeemLogRepository.save(log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
user-service/src/main/resources/application-dev.yml
Normal file
19
user-service/src/main/resources/application-dev.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
server:
|
||||
port: 8601
|
||||
|
||||
giftcode:
|
||||
service:
|
||||
url: http://localhost:8602
|
||||
|
||||
internal:
|
||||
api-key: "dken-secret-key-2024"
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: user-service
|
||||
data:
|
||||
mongodb:
|
||||
uri: mongodb://localhost:27017/userdb
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
@@ -1,16 +1,19 @@
|
||||
server:
|
||||
port: 8601
|
||||
|
||||
giftcode:
|
||||
service:
|
||||
url: http://localhost:8602
|
||||
url: http://giftcode-service:8602
|
||||
|
||||
internal:
|
||||
api-key: "dken-secret-key-2024"
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: user-service
|
||||
data:
|
||||
mongodb:
|
||||
host: localhost
|
||||
port: 27017
|
||||
database: userdb
|
||||
uri: mongodb://mongodb:27017/userdb
|
||||
redis:
|
||||
host: localhost
|
||||
host: redis
|
||||
port: 6379
|
||||
Reference in New Issue
Block a user