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