diff --git a/docker-compose.yml b/docker-compose.yml
index 048e51e..197f279 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,8 +10,6 @@ services:
build: ./user-service
ports:
- "8601:8601"
- environment:
- - SPRING_PROFILES_ACTIVE=docker
depends_on:
- redis
- mongodb
@@ -20,8 +18,6 @@ services:
build: ./giftcode-service
ports:
- "8602:8602"
- environment:
- - SPRING_PROFILES_ACTIVE=docker
depends_on:
- redis
- mongodb
diff --git a/giftCode-service/pom.xml b/giftCode-service/pom.xml
index 54c5e2a..9e6055c 100644
--- a/giftCode-service/pom.xml
+++ b/giftCode-service/pom.xml
@@ -9,9 +9,9 @@
com.dken
- gift-code-service
+ giftcode-service
0.0.1-SNAPSHOT
- gift-code-service
+ giftcode-service
Gift Code Service
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/config/InternalAuthFilter.java b/giftCode-service/src/main/java/com/dken/giftCodeService/config/InternalAuthFilter.java
new file mode 100644
index 0000000..c00e053
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/config/InternalAuthFilter.java
@@ -0,0 +1,41 @@
+package com.dken.giftcodeservice.config;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@Order(1)
+public class InternalAuthFilter implements Filter {
+
+ @Value("${internal.api-key}")
+ private String internalApiKey;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String path = httpRequest.getRequestURI();
+
+ if (path.startsWith("/v3/api-docs") || path.startsWith("/swagger-ui")) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ String apiKey = httpRequest.getHeader("X-Internal-Key");
+
+ if (internalApiKey.equals(apiKey)) {
+ chain.doFilter(request, response);
+ } else {
+ httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ httpResponse.getWriter().write("Unauthorized: Invalid Internal API Key");
+ }
+ }
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/config/MongoConfig.java b/giftCode-service/src/main/java/com/dken/giftCodeService/config/MongoConfig.java
new file mode 100644
index 0000000..8e64361
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/config/MongoConfig.java
@@ -0,0 +1,34 @@
+package com.dken.giftcodeservice.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/giftcodedb}")
+ 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);
+ }
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/controller/GiftCodeController.java b/giftCode-service/src/main/java/com/dken/giftCodeService/controller/GiftCodeController.java
index 6fb0387..ff529eb 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/controller/GiftCodeController.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/controller/GiftCodeController.java
@@ -1,10 +1,9 @@
-package com.dken.giftCodeService.controller;
+package com.dken.giftcodeservice.controller;
-import com.dken.giftCodeService.dto.request.GiftCodeCreateRequest;
-import com.dken.giftCodeService.dto.request.UserApplyGiftCodeRequest;
-import com.dken.giftCodeService.dto.response.GiftCodeResponse;
-import com.dken.giftCodeService.dto.response.UserApplyGiftCodeResponse;
-import com.dken.giftCodeService.service.GiftCodeService;
+import com.dken.giftcodeservice.dto.request.GiftCodeCreateRequest;
+import com.dken.giftcodeservice.dto.request.GiftCodeTransactionRequest;
+import com.dken.giftcodeservice.dto.response.GiftCodeResponse;
+import com.dken.giftcodeservice.service.GiftCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
@@ -13,6 +12,8 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
+import java.util.Map;
+
@RestController
@RequestMapping("/gift-codes")
public class GiftCodeController {
@@ -36,8 +37,18 @@ public class GiftCodeController {
return giftCodeService.getGiftCodeByCode(code);
}
- @PostMapping("/apply")
- public UserApplyGiftCodeResponse applyGiftCode(@Valid @RequestBody UserApplyGiftCodeRequest req) {
- return giftCodeService.applyGiftCode(req.getGiftCode(), req.getUserId());
+ @PostMapping("/reserve")
+ public Integer reserve(@Valid @RequestBody GiftCodeTransactionRequest req) {
+ return giftCodeService.reserve(req);
+ }
+
+ @PostMapping("/confirm")
+ public void confirm(@RequestBody Map body) {
+ giftCodeService.confirm(body.get("transactionId"));
+ }
+
+ @PostMapping("/cancel")
+ public void cancel(@RequestBody Map body) {
+ giftCodeService.cancel(body.get("transactionId"));
}
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeCreateRequest.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeCreateRequest.java
index a25aecf..e52ada8 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeCreateRequest.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeCreateRequest.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.dto.request;
+package com.dken.giftcodeservice.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -23,12 +23,13 @@ public class GiftCodeCreateRequest {
private boolean active;
private Instant startAt;
private Instant endAt;
+ private int amount;
public GiftCodeCreateRequest() {
}
public GiftCodeCreateRequest(String code, String type, int usage, int limit, boolean multiUse, boolean onePerType,
- Instant startAt, Instant endAt) {
+ Instant startAt, Instant endAt, int amount) {
this.code = code;
this.type = type;
this.usage = usage;
@@ -37,6 +38,7 @@ public class GiftCodeCreateRequest {
this.onePerType = onePerType;
this.startAt = startAt;
this.endAt = endAt;
+ this.amount = amount;
}
// getter only
@@ -75,4 +77,8 @@ public class GiftCodeCreateRequest {
public Instant getEndAt() {
return endAt;
}
+
+ public int getAmount() {
+ return amount;
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeTransactionRequest.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeTransactionRequest.java
new file mode 100644
index 0000000..9312ab3
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/GiftCodeTransactionRequest.java
@@ -0,0 +1,43 @@
+package com.dken.giftcodeservice.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+
+public class GiftCodeTransactionRequest {
+ @NotBlank
+ private String transactionId;
+ private String userId;
+ private String code;
+
+ public GiftCodeTransactionRequest() {
+ }
+
+ public GiftCodeTransactionRequest(String transactionId, String userId, String code) {
+ this.transactionId = transactionId;
+ this.userId = userId;
+ this.code = code;
+ }
+
+ 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;
+ }
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/UserApplyGiftCodeRequest.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/UserApplyGiftCodeRequest.java
index d2cb7d9..f0d510d 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/UserApplyGiftCodeRequest.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/request/UserApplyGiftCodeRequest.java
@@ -1,12 +1,14 @@
-package com.dken.giftCodeService.dto.request;
+package com.dken.giftcodeservice.dto.request;
public class UserApplyGiftCodeRequest {
private String userId;
private String giftCode;
+ private String requestId;
- public UserApplyGiftCodeRequest(String userId, String giftCode) {
+ public UserApplyGiftCodeRequest(String userId, String giftCode, String requestId) {
this.userId = userId;
this.giftCode = giftCode;
+ this.requestId = requestId;
}
public String getUserId() {
@@ -24,4 +26,12 @@ public class UserApplyGiftCodeRequest {
public void setGiftCode(String giftCode) {
this.giftCode = giftCode;
}
+
+ public String getRequestId() {
+ return requestId;
+ }
+
+ public void setRequestId(String requestId) {
+ this.requestId = requestId;
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/GiftCodeResponse.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/GiftCodeResponse.java
index 8466fa8..78eccfe 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/GiftCodeResponse.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/GiftCodeResponse.java
@@ -1,8 +1,8 @@
-package com.dken.giftCodeService.dto.response;
+package com.dken.giftcodeservice.dto.response;
import java.time.Instant;
-import com.dken.giftCodeService.model.GiftCode;
+import com.dken.giftcodeservice.model.GiftCode;
public class GiftCodeResponse {
@@ -16,15 +16,17 @@ public class GiftCodeResponse {
private Instant endAt;
private Instant createdAt;
private boolean active;
+ private int amount;
public static GiftCodeResponse from(GiftCode giftCode) {
return new GiftCodeResponse(giftCode.getCode(), giftCode.isMultiUse(), giftCode.isOnePerType(),
giftCode.getType(), giftCode.getUsageLimit(), giftCode.getUsageCount(), giftCode.isActive(),
- giftCode.getStartAt(), giftCode.getEndAt(), giftCode.getCreatedAt());
+ giftCode.getStartAt(), giftCode.getEndAt(), giftCode.getCreatedAt(), giftCode.getAmount());
}
public GiftCodeResponse(String code, boolean multiUse, boolean onePerType, String type,
- Integer usageLimit, Integer usageCount, boolean active, Instant startAt, Instant endAt, Instant createdAt) {
+ Integer usageLimit, Integer usageCount, boolean active, Instant startAt, Instant endAt, Instant createdAt,
+ int amount) {
this.code = code;
this.multiUse = multiUse;
this.onePerType = onePerType;
@@ -35,6 +37,7 @@ public class GiftCodeResponse {
this.startAt = startAt;
this.endAt = endAt;
this.createdAt = createdAt;
+ this.amount = amount;
}
// getter only
@@ -77,4 +80,8 @@ public class GiftCodeResponse {
public Instant getCreatedAt() {
return createdAt;
}
+
+ public int getAmount() {
+ return amount;
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserApplyGiftCodeResponse.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserApplyGiftCodeResponse.java
index 5573225..7594060 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserApplyGiftCodeResponse.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserApplyGiftCodeResponse.java
@@ -1,17 +1,19 @@
-package com.dken.giftCodeService.dto.response;
+package com.dken.giftcodeservice.dto.response;
public class UserApplyGiftCodeResponse {
private String message;
private String status;
- private String description;
+ private Integer amount;
+ private String requestId;
public UserApplyGiftCodeResponse() {
}
- public UserApplyGiftCodeResponse(String message, String status, String description) {
+ public UserApplyGiftCodeResponse(String message, String status, Integer amount, String requestId) {
this.message = message;
this.status = status;
- this.description = description;
+ this.amount = amount;
+ this.requestId = requestId;
}
// getters and setters
@@ -31,11 +33,19 @@ public class UserApplyGiftCodeResponse {
this.status = status;
}
- public String getDescription() {
- return description;
+ public String getRequestId() {
+ return requestId;
}
- public void setDescription(String description) {
- this.description = description;
+ public void setRequestId(String requestId) {
+ this.requestId = requestId;
+ }
+
+ public Integer getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Integer amount) {
+ this.amount = amount;
}
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserConfirmGiftCodeResponse.java b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserConfirmGiftCodeResponse.java
index 96a96c6..ae98592 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserConfirmGiftCodeResponse.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/dto/response/UserConfirmGiftCodeResponse.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.dto.response;
+package com.dken.giftcodeservice.dto.response;
public class UserConfirmGiftCodeResponse {
private String userId;
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/exception/DefaultException.java b/giftCode-service/src/main/java/com/dken/giftCodeService/exception/DefaultException.java
index b296c94..dd0ca74 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/exception/DefaultException.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/exception/DefaultException.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.exception;
+package com.dken.giftcodeservice.exception;
public class DefaultException extends RuntimeException {
public DefaultException(String message) {
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/exception/GlobalExceptionHandler.java b/giftCode-service/src/main/java/com/dken/giftCodeService/exception/GlobalExceptionHandler.java
index 16a9110..f9dfdc8 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/exception/GlobalExceptionHandler.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/exception/GlobalExceptionHandler.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.exception;
+package com.dken.giftcodeservice.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/giftCodeServiceApplication.java b/giftCode-service/src/main/java/com/dken/giftCodeService/giftCodeServiceApplication.java
index c003001..0fbf8a0 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/giftCodeServiceApplication.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/giftCodeServiceApplication.java
@@ -1,15 +1,19 @@
-package com.dken.giftCodeService;
+package com.dken.giftcodeservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
+import org.springframework.data.web.config.EnableSpringDataWebSupport;
+import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
@SpringBootApplication
@EnableMongoAuditing
-public class giftCodeServiceApplication {
+@org.springframework.scheduling.annotation.EnableScheduling
+@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
+public class GiftCodeServiceApplication {
public static void main(String[] args) {
- SpringApplication.run(giftCodeServiceApplication.class, args);
+ SpringApplication.run(GiftCodeServiceApplication.class, args);
}
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCode.java b/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCode.java
index b761d3e..cb2dca3 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCode.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCode.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.model;
+package com.dken.giftcodeservice.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@@ -26,6 +26,7 @@ public class GiftCode {
private Instant startAt;
private Instant endAt;
private boolean active;
+ private Integer amount;
@CreatedDate
private Instant createdAt;
@@ -39,7 +40,7 @@ public class GiftCode {
}
public GiftCode(String code, boolean multiUse, boolean onePerType, String type, Integer usageLimit,
- Instant startAt, Instant endAt) {
+ Instant startAt, Instant endAt, Integer amount) {
this.code = code;
this.multiUse = multiUse;
this.onePerType = onePerType;
@@ -49,6 +50,7 @@ public class GiftCode {
this.startAt = startAt;
this.endAt = endAt;
this.active = true;
+ this.amount = amount;
}
// ===== business methods =====
@@ -174,4 +176,12 @@ public class GiftCode {
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
+
+ public Integer getAmount() {
+ return amount == null ? 0 : amount;
+ }
+
+ public void setAmount(Integer amount) {
+ this.amount = amount;
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCodeUsage.java b/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCodeUsage.java
index 28d4cae..10649b6 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCodeUsage.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/model/GiftCodeUsage.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.model;
+package com.dken.giftcodeservice.model;
import java.time.Instant;
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemStatus.java b/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemStatus.java
new file mode 100644
index 0000000..b6b9f92
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemStatus.java
@@ -0,0 +1,7 @@
+package com.dken.giftcodeservice.model;
+
+public enum RedeemStatus {
+ RESERVED,
+ USED,
+ CANCELLED
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemTransaction.java b/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemTransaction.java
new file mode 100644
index 0000000..f6086f0
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/model/RedeemTransaction.java
@@ -0,0 +1,105 @@
+package com.dken.giftcodeservice.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_transactions")
+public class RedeemTransaction {
+
+ @Id
+ private String id;
+
+ @Indexed(unique = true)
+ private String transactionId;
+
+ @Indexed
+ private String code;
+
+ @Indexed
+ private String userId;
+
+ private RedeemStatus status;
+
+ private Instant reservedAt;
+ private Instant usedAt;
+ private Instant cancelledAt;
+
+ public RedeemTransaction() {
+ }
+
+ public RedeemTransaction(String transactionId, String code, String userId, RedeemStatus status,
+ Instant reservedAt) {
+ this.transactionId = transactionId;
+ this.code = code;
+ this.userId = userId;
+ this.status = status;
+ this.reservedAt = reservedAt;
+ }
+
+ 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 getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public RedeemStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(RedeemStatus status) {
+ this.status = status;
+ }
+
+ public Instant getReservedAt() {
+ return reservedAt;
+ }
+
+ public void setReservedAt(Instant reservedAt) {
+ this.reservedAt = reservedAt;
+ }
+
+ public Instant getUsedAt() {
+ return usedAt;
+ }
+
+ public void setUsedAt(Instant usedAt) {
+ this.usedAt = usedAt;
+ }
+
+ public Instant getCancelledAt() {
+ return cancelledAt;
+ }
+
+ public void setCancelledAt(Instant cancelledAt) {
+ this.cancelledAt = cancelledAt;
+ }
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepository.java b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepository.java
index efab525..256b136 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepository.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepository.java
@@ -1,7 +1,7 @@
-package com.dken.giftCodeService.repository;
+package com.dken.giftcodeservice.repository;
-import com.dken.giftCodeService.model.GiftCode;
+import com.dken.giftcodeservice.model.GiftCode;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@@ -11,4 +11,4 @@ import java.util.Optional;
public interface GiftCodeRepository extends MongoRepository {
Optional findByCode(String code);
-}
\ No newline at end of file
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryCustom.java b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryCustom.java
index a4eaa73..25aa38c 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryCustom.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryCustom.java
@@ -1,5 +1,7 @@
-package com.dken.giftCodeService.repository;
+package com.dken.giftcodeservice.repository;
public interface GiftCodeRepositoryCustom {
boolean increaseUsageIfAvailable(String code);
+
+ void decreaseUsage(String code);
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryImpl.java b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryImpl.java
index 240aba1..315142c 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryImpl.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeRepositoryImpl.java
@@ -1,6 +1,6 @@
-package com.dken.giftCodeService.repository;
+package com.dken.giftcodeservice.repository;
-import com.dken.giftCodeService.model.GiftCode;
+import com.dken.giftcodeservice.model.GiftCode;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
@@ -12,32 +12,40 @@ import org.springframework.stereotype.Repository;
@Repository
public class GiftCodeRepositoryImpl implements GiftCodeRepositoryCustom {
- private final MongoTemplate mongoTemplate;
+ private final MongoTemplate mongoTemplate;
- public GiftCodeRepositoryImpl(MongoTemplate mongoTemplate) {
- this.mongoTemplate = mongoTemplate;
- }
+ public GiftCodeRepositoryImpl(MongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ }
- @Override
- public boolean increaseUsageIfAvailable(String code) {
- Query query = new Query(
- new Criteria().andOperator(
- Criteria.where("code").is(code),
- Criteria.where("active").is(true),
- new Criteria().orOperator(
- Criteria.where("usageLimit").is(null),
- Criteria.expr(
- ComparisonOperators.Lt.valueOf("usageCount")
- .lessThan("usageLimit")))));
+ @Override
+ public boolean increaseUsageIfAvailable(String code) {
+ Query query = new Query(
+ new Criteria().andOperator(
+ Criteria.where("code").is(code),
+ Criteria.where("active").is(true),
+ new Criteria().orOperator(
+ Criteria.where("usageLimit").is(null),
+ Criteria.expr(
+ ComparisonOperators.Lt
+ .valueOf("usageCount")
+ .lessThan("usageLimit")))));
- Update update = new Update().inc("usageCount", 1);
+ Update update = new Update().inc("usageCount", 1);
- GiftCode updated = mongoTemplate.findAndModify(
- query,
- update,
- FindAndModifyOptions.options().returnNew(true),
- GiftCode.class);
+ GiftCode updated = mongoTemplate.findAndModify(
+ query,
+ update,
+ FindAndModifyOptions.options().returnNew(true),
+ GiftCode.class);
- return updated != null;
- }
+ return updated != null;
+ }
+
+ @Override
+ public void decreaseUsage(String code) {
+ Query query = new Query(Criteria.where("code").is(code).and("usageCount").gt(0));
+ Update update = new Update().inc("usageCount", -1);
+ mongoTemplate.updateFirst(query, update, GiftCode.class);
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeUsageRepository.java b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeUsageRepository.java
index 4098afe..f236062 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeUsageRepository.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/GiftCodeUsageRepository.java
@@ -1,6 +1,6 @@
-package com.dken.giftCodeService.repository;
+package com.dken.giftcodeservice.repository;
-import com.dken.giftCodeService.model.GiftCodeUsage;
+import com.dken.giftcodeservice.model.GiftCodeUsage;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/repository/RedeemTransactionRepository.java b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/RedeemTransactionRepository.java
new file mode 100644
index 0000000..4fef3e6
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/repository/RedeemTransactionRepository.java
@@ -0,0 +1,17 @@
+package com.dken.giftcodeservice.repository;
+
+import com.dken.giftcodeservice.model.RedeemTransaction;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.Query;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+public interface RedeemTransactionRepository extends MongoRepository {
+ Optional findByTransactionId(String transactionId);
+
+ // Find reserved transactions older than a certain time
+ @Query("{ 'status': 'RESERVED', 'reservedAt': { $lt: ?0 } }")
+ List findStuckReservedTransactions(Instant timeLimit);
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/scheduler/ReclaimScheduler.java b/giftCode-service/src/main/java/com/dken/giftCodeService/scheduler/ReclaimScheduler.java
new file mode 100644
index 0000000..1f525cf
--- /dev/null
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/scheduler/ReclaimScheduler.java
@@ -0,0 +1,40 @@
+package com.dken.giftcodeservice.scheduler;
+
+import com.dken.giftcodeservice.model.RedeemTransaction;
+import com.dken.giftcodeservice.repository.RedeemTransactionRepository;
+import com.dken.giftcodeservice.service.GiftCodeService;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+@Component
+public class ReclaimScheduler {
+
+ private final RedeemTransactionRepository redeemTransactionRepository;
+ private final GiftCodeService giftCodeService;
+
+ public ReclaimScheduler(RedeemTransactionRepository redeemTransactionRepository, GiftCodeService giftCodeService) {
+ this.redeemTransactionRepository = redeemTransactionRepository;
+ this.giftCodeService = giftCodeService;
+ }
+
+ @Scheduled(fixedDelay = 60000) // 1 minute
+ public void reclaim() {
+ Instant fiveMinutesAgo = Instant.now().minus(5, ChronoUnit.MINUTES);
+ List stuckTransactions = redeemTransactionRepository
+ .findStuckReservedTransactions(fiveMinutesAgo);
+
+ for (RedeemTransaction tx : stuckTransactions) {
+ try {
+ giftCodeService.cancel(tx.getTransactionId());
+ System.out.println("Reclaimed stuck transaction: " + tx.getTransactionId());
+ } catch (Exception e) {
+ System.err.println("Failed to reclaim transaction: " + tx.getTransactionId());
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/service/GiftCodeService.java b/giftCode-service/src/main/java/com/dken/giftCodeService/service/GiftCodeService.java
index a66ac41..4e3a7e2 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/service/GiftCodeService.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/service/GiftCodeService.java
@@ -1,15 +1,17 @@
-package com.dken.giftCodeService.service;
-
-import com.dken.giftCodeService.model.GiftCode;
-import com.dken.giftCodeService.model.GiftCodeUsage;
-import com.dken.giftCodeService.exception.DefaultException;
-import com.dken.giftCodeService.repository.GiftCodeRepository;
-import com.dken.giftCodeService.repository.GiftCodeRepositoryImpl;
-import com.dken.giftCodeService.repository.GiftCodeUsageRepository;
-import com.dken.giftCodeService.dto.request.GiftCodeCreateRequest;
-import com.dken.giftCodeService.dto.response.GiftCodeResponse;
-import com.dken.giftCodeService.dto.response.UserApplyGiftCodeResponse;
+package com.dken.giftcodeservice.service;
+import com.dken.giftcodeservice.model.GiftCode;
+import com.dken.giftcodeservice.model.GiftCodeUsage;
+import com.dken.giftcodeservice.model.RedeemStatus;
+import com.dken.giftcodeservice.model.RedeemTransaction;
+import com.dken.giftcodeservice.dto.request.GiftCodeCreateRequest;
+import com.dken.giftcodeservice.dto.request.GiftCodeTransactionRequest;
+import com.dken.giftcodeservice.dto.response.GiftCodeResponse;
+import com.dken.giftcodeservice.exception.DefaultException;
+import com.dken.giftcodeservice.repository.GiftCodeRepository;
+import com.dken.giftcodeservice.repository.GiftCodeRepositoryImpl;
+import com.dken.giftcodeservice.repository.GiftCodeUsageRepository;
+import com.dken.giftcodeservice.repository.RedeemTransactionRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -20,6 +22,9 @@ import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
import java.util.UUID;
@Service
@@ -28,37 +33,35 @@ public class GiftCodeService {
private final GiftCodeRepository giftCodeRepository;
private final GiftCodeRepositoryImpl giftCodeRepositoryImpl;
private final GiftCodeUsageRepository usageRepo;
+ private final RedeemTransactionRepository redeemTransactionRepository;
private final RedisLockService lockService;
public GiftCodeService(GiftCodeRepository giftCodeRepository,
GiftCodeRepositoryImpl giftCodeRepositoryImpl,
GiftCodeUsageRepository usageRepo,
+ RedeemTransactionRepository redeemTransactionRepository,
RedisLockService lockService) {
this.giftCodeRepository = giftCodeRepository;
this.giftCodeRepositoryImpl = giftCodeRepositoryImpl;
this.usageRepo = usageRepo;
+ this.redeemTransactionRepository = redeemTransactionRepository;
this.lockService = lockService;
}
public Page getAllGiftCodes(int page, int size) {
-
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
-
- return giftCodeRepository.findAll(pageable)
- .map(GiftCodeResponse::from);
+ return giftCodeRepository.findAll(pageable).map(GiftCodeResponse::from);
}
private static String hashType(String type) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(type.getBytes(StandardCharsets.UTF_8));
-
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
-
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
@@ -66,21 +69,14 @@ public class GiftCodeService {
String generateCode(String type) {
String typeHash = hashType(type).substring(0, 6);
- String randomPart = UUID.randomUUID()
- .toString()
- .replace("-", "")
- .substring(0, 16)
- .toUpperCase();
-
+ String randomPart = UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
return (typeHash + randomPart).toUpperCase();
}
public GiftCode create(GiftCodeCreateRequest req) {
if (req.getCode() == null || req.getCode().isEmpty()) {
- String code = generateCode(req.getType());
- req.setCode(code);
+ req.setCode(generateCode(req.getType()));
}
-
GiftCode giftCode = new GiftCode();
giftCode.setCode(req.getCode());
giftCode.setMultiUse(req.isMultiUse());
@@ -91,12 +87,12 @@ public class GiftCodeService {
giftCode.setActive(req.isActive());
giftCode.setStartAt(req.getStartAt());
giftCode.setEndAt(req.getEndAt());
+ giftCode.setAmount(req.getAmount());
return giftCodeRepository.save(giftCode);
}
public GiftCodeResponse createGiftCode(GiftCodeCreateRequest req) {
- GiftCode giftCode = create(req);
- return GiftCodeResponse.from(giftCode);
+ return GiftCodeResponse.from(create(req));
}
public GiftCodeResponse getGiftCodeByCode(String code) {
@@ -105,59 +101,98 @@ public class GiftCodeService {
return GiftCodeResponse.from(giftCode);
}
- public UserApplyGiftCodeResponse applyGiftCode(String code, String userId) {
- String lockKey = "lock:giftcode:" + code + ":user:" + userId;
- try {
- if (!lockService.lock(lockKey, java.time.Duration.ofSeconds(5))) {
- throw new DefaultException("Please wait and try again");
+ @Transactional
+ public Integer reserve(GiftCodeTransactionRequest req) {
+ Optional txOpt = redeemTransactionRepository.findByTransactionId(req.getTransactionId());
+ if (txOpt.isPresent()) {
+ RedeemTransaction tx = txOpt.get();
+ if (tx.getStatus() == RedeemStatus.CANCELLED) {
+ throw new DefaultException("Transaction already cancelled");
}
+ GiftCode code = giftCodeRepository.findByCode(req.getCode()).orElseThrow();
+ return code.getAmount();
+ }
- GiftCode giftCode = giftCodeRepository.findByCode(code)
+ String lockKey = "lock:reserve:" + req.getUserId();
+ if (!lockService.lock(lockKey, Duration.ofSeconds(5))) {
+ throw new DefaultException("System busy");
+ }
+
+ try {
+ GiftCode giftCode = giftCodeRepository.findByCode(req.getCode())
.orElseThrow(() -> new RuntimeException("Gift code not found"));
+
if (!giftCode.canUse()) {
throw new DefaultException("Gift code is not available");
}
- if (!giftCode.isMultiUse()) {
- boolean used = usageRepo.existsByGiftCodeAndUserId(
- giftCode.getCode(), userId);
- if (used) {
- throw new DefaultException("Gift code has already been used");
- }
+ checkUsageHistory(giftCode, req.getUserId());
+
+ boolean increased = giftCodeRepositoryImpl.increaseUsageIfAvailable(giftCode.getCode());
+ if (!increased) {
+ throw new DefaultException("Gift code is out of limit");
}
- if (giftCode.isOnePerType()) {
- boolean used = usageRepo.existsByUserIdAndTypeAndGiftCodeNotAndOnePerTypeTrue(userId,
- giftCode.getType(),
- giftCode.getCode());
- if (used) {
- throw new DefaultException("Gift code has already been used");
- }
- }
- effectGiftCode(giftCode, userId);
- return new UserApplyGiftCodeResponse("Gift code applied successfully", "success", "");
- } catch (Exception e) {
- return new UserApplyGiftCodeResponse(
- e.getMessage(),
- "failed",
- "ROLLBACK");
+
+ RedeemTransaction tx = new RedeemTransaction(req.getTransactionId(), req.getCode(), req.getUserId(),
+ RedeemStatus.RESERVED, Instant.now());
+ redeemTransactionRepository.save(tx);
+
+ return giftCode.getAmount();
} finally {
lockService.unlock(lockKey);
}
}
@Transactional
- void effectGiftCode(GiftCode giftCode, String userId) {
- boolean increased = giftCodeRepositoryImpl
- .increaseUsageIfAvailable(giftCode.getCode());
+ public void confirm(String transactionId) {
+ RedeemTransaction tx = redeemTransactionRepository.findByTransactionId(transactionId)
+ .orElseThrow(() -> new DefaultException("Transaction not found"));
- if (!increased) {
- throw new DefaultException("Gift code is out of limit");
- }
+ if (tx.getStatus() == RedeemStatus.USED)
+ return;
+ if (tx.getStatus() == RedeemStatus.CANCELLED)
+ throw new DefaultException("Transaction cancelled");
- GiftCodeUsage usage = new GiftCodeUsage(giftCode.getId(), giftCode.getCode(), giftCode.isOnePerType(),
- userId);
+ tx.setStatus(RedeemStatus.USED);
+ tx.setUsedAt(Instant.now());
+ redeemTransactionRepository.save(tx);
+ GiftCode giftCode = giftCodeRepository.findByCode(tx.getCode()).orElseThrow();
+ GiftCodeUsage usage = new GiftCodeUsage(giftCode.getCode(), giftCode.getType(), giftCode.isOnePerType(),
+ tx.getUserId());
usageRepo.save(usage);
}
+ @Transactional
+ public void cancel(String transactionId) {
+ RedeemTransaction tx = redeemTransactionRepository.findByTransactionId(transactionId)
+ .orElseThrow(() -> new DefaultException("Transaction not found"));
+
+ if (tx.getStatus() == RedeemStatus.CANCELLED)
+ return;
+ if (tx.getStatus() == RedeemStatus.USED)
+ throw new DefaultException("Cannot cancel used transaction");
+
+ tx.setStatus(RedeemStatus.CANCELLED);
+ tx.setCancelledAt(Instant.now());
+ redeemTransactionRepository.save(tx);
+
+ giftCodeRepositoryImpl.decreaseUsage(tx.getCode());
+ }
+
+ private void checkUsageHistory(GiftCode giftCode, String userId) {
+ if (!giftCode.isMultiUse()) {
+ boolean used = usageRepo.existsByGiftCodeAndUserId(giftCode.getCode(), userId);
+ if (used) {
+ throw new DefaultException("Gift code has already been used");
+ }
+ }
+ if (giftCode.isOnePerType()) {
+ boolean used = usageRepo.existsByUserIdAndTypeAndGiftCodeNotAndOnePerTypeTrue(userId, giftCode.getType(),
+ giftCode.getCode());
+ if (used) {
+ throw new DefaultException("Gift code type has already been used");
+ }
+ }
+ }
}
diff --git a/giftCode-service/src/main/java/com/dken/giftCodeService/service/RedisLockService.java b/giftCode-service/src/main/java/com/dken/giftCodeService/service/RedisLockService.java
index 3381bd3..8b12d0a 100644
--- a/giftCode-service/src/main/java/com/dken/giftCodeService/service/RedisLockService.java
+++ b/giftCode-service/src/main/java/com/dken/giftCodeService/service/RedisLockService.java
@@ -1,4 +1,4 @@
-package com.dken.giftCodeService.service;
+package com.dken.giftcodeservice.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
diff --git a/giftCode-service/src/main/resources/application-dev.yml b/giftCode-service/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..6177856
--- /dev/null
+++ b/giftCode-service/src/main/resources/application-dev.yml
@@ -0,0 +1,17 @@
+server:
+ port: 8602
+
+internal:
+ api-key: "dken-secret-key-2024"
+
+spring:
+ application:
+ name: giftcode-service
+ data:
+ mongodb:
+ host: localhost
+ port: 27017
+ database: giftcodedb
+ redis:
+ host: localhost
+ port: 6379
\ No newline at end of file
diff --git a/giftCode-service/src/main/resources/application.yml b/giftCode-service/src/main/resources/application.yml
index fe3a926..71f13c1 100644
--- a/giftCode-service/src/main/resources/application.yml
+++ b/giftCode-service/src/main/resources/application.yml
@@ -1,14 +1,17 @@
server:
port: 8602
+internal:
+ api-key: "dken-secret-key-2024"
+
spring:
application:
- name: gift-code-service
+ name: giftcode-service
data:
mongodb:
- host: localhost
+ host: mongodb
port: 27017
- database: gift-codedb
+ database: giftcodedb
redis:
- host: localhost
+ host: redis
port: 6379
\ No newline at end of file
diff --git a/giftCode-service/src/test/java/com/dken/giftcodeservice/GiftCodeServiceApplicationTests.java b/giftCode-service/src/test/java/com/dken/giftcodeservice/GiftCodeServiceApplicationTests.java
new file mode 100644
index 0000000..ea2da0c
--- /dev/null
+++ b/giftCode-service/src/test/java/com/dken/giftcodeservice/GiftCodeServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.dken.giftcodeservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GiftCodeServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/user-service/Dockerfile b/user-service/Dockerfile
index ca00a32..defb2d4 100644
--- a/user-service/Dockerfile
+++ b/user-service/Dockerfile
@@ -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"]
diff --git a/user-service/src/main/java/com/dken/userservice/UserServiceApplication.java b/user-service/src/main/java/com/dken/userservice/UserServiceApplication.java
index a877cc0..26db4b6 100644
--- a/user-service/src/main/java/com/dken/userservice/UserServiceApplication.java
+++ b/user-service/src/main/java/com/dken/userservice/UserServiceApplication.java
@@ -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);
diff --git a/user-service/src/main/java/com/dken/userservice/client/GiftCodeClient.java b/user-service/src/main/java/com/dken/userservice/client/GiftCodeClient.java
index 1c93200..1b2afdd 100644
--- a/user-service/src/main/java/com/dken/userservice/client/GiftCodeClient.java
+++ b/user-service/src/main/java/com/dken/userservice/client/GiftCodeClient.java
@@ -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();
}
}
diff --git a/user-service/src/main/java/com/dken/userservice/config/GiftCodeClientConfig.java b/user-service/src/main/java/com/dken/userservice/config/GiftCodeClientConfig.java
index f0b57dd..35505ae 100644
--- a/user-service/src/main/java/com/dken/userservice/config/GiftCodeClientConfig.java
+++ b/user-service/src/main/java/com/dken/userservice/config/GiftCodeClientConfig.java
@@ -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);
})
diff --git a/user-service/src/main/java/com/dken/userservice/config/MongoConfig.java b/user-service/src/main/java/com/dken/userservice/config/MongoConfig.java
new file mode 100644
index 0000000..3170b07
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/config/MongoConfig.java
@@ -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);
+ }
+}
diff --git a/user-service/src/main/java/com/dken/userservice/dto/request/GiftCodeValidateRequest.java b/user-service/src/main/java/com/dken/userservice/dto/request/GiftCodeValidateRequest.java
index 6b949ba..37a94c2 100644
--- a/user-service/src/main/java/com/dken/userservice/dto/request/GiftCodeValidateRequest.java
+++ b/user-service/src/main/java/com/dken/userservice/dto/request/GiftCodeValidateRequest.java
@@ -1,6 +1,7 @@
package com.dken.userservice.dto.request;
public record GiftCodeValidateRequest(
- String userId,
- String giftCode) {
+ String userId,
+ String giftCode,
+ String requestId) {
}
diff --git a/user-service/src/main/java/com/dken/userservice/dto/response/UserApplyGiftCodeResponse.java b/user-service/src/main/java/com/dken/userservice/dto/response/UserApplyGiftCodeResponse.java
index a63a94d..3521971 100644
--- a/user-service/src/main/java/com/dken/userservice/dto/response/UserApplyGiftCodeResponse.java
+++ b/user-service/src/main/java/com/dken/userservice/dto/response/UserApplyGiftCodeResponse.java
@@ -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;
}
}
diff --git a/user-service/src/main/java/com/dken/userservice/dto/response/UserResponse.java b/user-service/src/main/java/com/dken/userservice/dto/response/UserResponse.java
index 29519d0..ba0c1c4 100644
--- a/user-service/src/main/java/com/dken/userservice/dto/response/UserResponse.java
+++ b/user-service/src/main/java/com/dken/userservice/dto/response/UserResponse.java
@@ -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;
+ }
}
diff --git a/user-service/src/main/java/com/dken/userservice/exception/GiftCodeReservationException.java b/user-service/src/main/java/com/dken/userservice/exception/GiftCodeReservationException.java
new file mode 100644
index 0000000..598e3cc
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/exception/GiftCodeReservationException.java
@@ -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);
+ }
+}
diff --git a/user-service/src/main/java/com/dken/userservice/exception/GlobalExceptionHandler.java b/user-service/src/main/java/com/dken/userservice/exception/GlobalExceptionHandler.java
index aa026a6..ad04b53 100644
--- a/user-service/src/main/java/com/dken/userservice/exception/GlobalExceptionHandler.java
+++ b/user-service/src/main/java/com/dken/userservice/exception/GlobalExceptionHandler.java
@@ -27,4 +27,22 @@ public class GlobalExceptionHandler {
public Map handleNotFound(UserNotFoundException ex) {
return Map.of("message", ex.getMessage());
}
+
+ @ExceptionHandler(GiftCodeReservationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Map handleGiftCodeReservation(GiftCodeReservationException ex) {
+ return Map.of("message", ex.getMessage());
+ }
+
+ @ExceptionHandler(LocalProcessingException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Map handleLocalProcessing(LocalProcessingException ex) {
+ return Map.of("message", ex.getMessage());
+ }
+
+ @ExceptionHandler(RuntimeException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Map handleRuntimeException(RuntimeException ex) {
+ return Map.of("message", "An unexpected error occurred: " + ex.getMessage());
+ }
}
diff --git a/user-service/src/main/java/com/dken/userservice/exception/LocalProcessingException.java b/user-service/src/main/java/com/dken/userservice/exception/LocalProcessingException.java
new file mode 100644
index 0000000..011fd91
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/exception/LocalProcessingException.java
@@ -0,0 +1,7 @@
+package com.dken.userservice.exception;
+
+public class LocalProcessingException extends RuntimeException {
+ public LocalProcessingException(String message) {
+ super("Local transaction failed: " + message);
+ }
+}
diff --git a/user-service/src/main/java/com/dken/userservice/model/RedeemLog.java b/user-service/src/main/java/com/dken/userservice/model/RedeemLog.java
new file mode 100644
index 0000000..c19dc1f
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/model/RedeemLog.java
@@ -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;
+ }
+}
diff --git a/user-service/src/main/java/com/dken/userservice/model/RedeemLogStatus.java b/user-service/src/main/java/com/dken/userservice/model/RedeemLogStatus.java
new file mode 100644
index 0000000..f1fc065
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/model/RedeemLogStatus.java
@@ -0,0 +1,7 @@
+package com.dken.userservice.model;
+
+public enum RedeemLogStatus {
+ SUCCESS,
+ PENDING_CONFIRM,
+ FAILED
+}
diff --git a/user-service/src/main/java/com/dken/userservice/model/User.java b/user-service/src/main/java/com/dken/userservice/model/User.java
index a7ce69a..6088a19 100644
--- a/user-service/src/main/java/com/dken/userservice/model/User.java
+++ b/user-service/src/main/java/com/dken/userservice/model/User.java
@@ -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;
}
diff --git a/user-service/src/main/java/com/dken/userservice/repository/RedeemLogRepository.java b/user-service/src/main/java/com/dken/userservice/repository/RedeemLogRepository.java
new file mode 100644
index 0000000..2fb470a
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/repository/RedeemLogRepository.java
@@ -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 {
+ List findByStatus(RedeemLogStatus status);
+
+ RedeemLog findByTransactionId(String transactionId);
+}
diff --git a/user-service/src/main/java/com/dken/userservice/scheduler/RetryConfirmScheduler.java b/user-service/src/main/java/com/dken/userservice/scheduler/RetryConfirmScheduler.java
new file mode 100644
index 0000000..98e92d2
--- /dev/null
+++ b/user-service/src/main/java/com/dken/userservice/scheduler/RetryConfirmScheduler.java
@@ -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 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());
+ }
+ }
+ }
+}
diff --git a/user-service/src/main/java/com/dken/userservice/service/UserService.java b/user-service/src/main/java/com/dken/userservice/service/UserService.java
index c43525a..5bf0602 100644
--- a/user-service/src/main/java/com/dken/userservice/service/UserService.java
+++ b/user-service/src/main/java/com/dken/userservice/service/UserService.java
@@ -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 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);
+ }
}
}
diff --git a/user-service/src/main/resources/application-dev.yml b/user-service/src/main/resources/application-dev.yml
new file mode 100644
index 0000000..3fb88ae
--- /dev/null
+++ b/user-service/src/main/resources/application-dev.yml
@@ -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
\ No newline at end of file
diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml
index 0034380..1e128b9 100644
--- a/user-service/src/main/resources/application.yml
+++ b/user-service/src/main/resources/application.yml
@@ -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
\ No newline at end of file