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