bổ sung thêm chức năng
This commit is contained in:
@@ -10,8 +10,6 @@ services:
|
|||||||
build: ./user-service
|
build: ./user-service
|
||||||
ports:
|
ports:
|
||||||
- "8601:8601"
|
- "8601:8601"
|
||||||
environment:
|
|
||||||
- SPRING_PROFILES_ACTIVE=docker
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- mongodb
|
- mongodb
|
||||||
@@ -20,8 +18,6 @@ services:
|
|||||||
build: ./giftcode-service
|
build: ./giftcode-service
|
||||||
ports:
|
ports:
|
||||||
- "8602:8602"
|
- "8602:8602"
|
||||||
environment:
|
|
||||||
- SPRING_PROFILES_ACTIVE=docker
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- mongodb
|
- mongodb
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.dken</groupId>
|
<groupId>com.dken</groupId>
|
||||||
<artifactId>gift-code-service</artifactId>
|
<artifactId>giftcode-service</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>gift-code-service</name>
|
<name>giftcode-service</name>
|
||||||
<description>Gift Code Service</description>
|
<description>Gift Code Service</description>
|
||||||
<url/>
|
<url/>
|
||||||
<licenses>
|
<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.GiftCodeCreateRequest;
|
||||||
import com.dken.giftCodeService.dto.request.UserApplyGiftCodeRequest;
|
import com.dken.giftcodeservice.dto.request.GiftCodeTransactionRequest;
|
||||||
import com.dken.giftCodeService.dto.response.GiftCodeResponse;
|
import com.dken.giftcodeservice.dto.response.GiftCodeResponse;
|
||||||
import com.dken.giftCodeService.dto.response.UserApplyGiftCodeResponse;
|
import com.dken.giftcodeservice.service.GiftCodeService;
|
||||||
import com.dken.giftCodeService.service.GiftCodeService;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -13,6 +12,8 @@ import jakarta.validation.Valid;
|
|||||||
import jakarta.validation.constraints.Max;
|
import jakarta.validation.constraints.Max;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/gift-codes")
|
@RequestMapping("/gift-codes")
|
||||||
public class GiftCodeController {
|
public class GiftCodeController {
|
||||||
@@ -36,8 +37,18 @@ public class GiftCodeController {
|
|||||||
return giftCodeService.getGiftCodeByCode(code);
|
return giftCodeService.getGiftCodeByCode(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/apply")
|
@PostMapping("/reserve")
|
||||||
public UserApplyGiftCodeResponse applyGiftCode(@Valid @RequestBody UserApplyGiftCodeRequest req) {
|
public Integer reserve(@Valid @RequestBody GiftCodeTransactionRequest req) {
|
||||||
return giftCodeService.applyGiftCode(req.getGiftCode(), req.getUserId());
|
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.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -23,12 +23,13 @@ public class GiftCodeCreateRequest {
|
|||||||
private boolean active;
|
private boolean active;
|
||||||
private Instant startAt;
|
private Instant startAt;
|
||||||
private Instant endAt;
|
private Instant endAt;
|
||||||
|
private int amount;
|
||||||
|
|
||||||
public GiftCodeCreateRequest() {
|
public GiftCodeCreateRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiftCodeCreateRequest(String code, String type, int usage, int limit, boolean multiUse, boolean onePerType,
|
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.code = code;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.usage = usage;
|
this.usage = usage;
|
||||||
@@ -37,6 +38,7 @@ public class GiftCodeCreateRequest {
|
|||||||
this.onePerType = onePerType;
|
this.onePerType = onePerType;
|
||||||
this.startAt = startAt;
|
this.startAt = startAt;
|
||||||
this.endAt = endAt;
|
this.endAt = endAt;
|
||||||
|
this.amount = amount;
|
||||||
}
|
}
|
||||||
// getter only
|
// getter only
|
||||||
|
|
||||||
@@ -75,4 +77,8 @@ public class GiftCodeCreateRequest {
|
|||||||
public Instant getEndAt() {
|
public Instant getEndAt() {
|
||||||
return endAt;
|
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 {
|
public class UserApplyGiftCodeRequest {
|
||||||
private String userId;
|
private String userId;
|
||||||
private String giftCode;
|
private String giftCode;
|
||||||
|
private String requestId;
|
||||||
|
|
||||||
public UserApplyGiftCodeRequest(String userId, String giftCode) {
|
public UserApplyGiftCodeRequest(String userId, String giftCode, String requestId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.giftCode = giftCode;
|
this.giftCode = giftCode;
|
||||||
|
this.requestId = requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserId() {
|
public String getUserId() {
|
||||||
@@ -24,4 +26,12 @@ public class UserApplyGiftCodeRequest {
|
|||||||
public void setGiftCode(String giftCode) {
|
public void setGiftCode(String giftCode) {
|
||||||
this.giftCode = 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 java.time.Instant;
|
||||||
|
|
||||||
import com.dken.giftCodeService.model.GiftCode;
|
import com.dken.giftcodeservice.model.GiftCode;
|
||||||
|
|
||||||
public class GiftCodeResponse {
|
public class GiftCodeResponse {
|
||||||
|
|
||||||
@@ -16,15 +16,17 @@ public class GiftCodeResponse {
|
|||||||
private Instant endAt;
|
private Instant endAt;
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
private int amount;
|
||||||
|
|
||||||
public static GiftCodeResponse from(GiftCode giftCode) {
|
public static GiftCodeResponse from(GiftCode giftCode) {
|
||||||
return new GiftCodeResponse(giftCode.getCode(), giftCode.isMultiUse(), giftCode.isOnePerType(),
|
return new GiftCodeResponse(giftCode.getCode(), giftCode.isMultiUse(), giftCode.isOnePerType(),
|
||||||
giftCode.getType(), giftCode.getUsageLimit(), giftCode.getUsageCount(), giftCode.isActive(),
|
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,
|
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.code = code;
|
||||||
this.multiUse = multiUse;
|
this.multiUse = multiUse;
|
||||||
this.onePerType = onePerType;
|
this.onePerType = onePerType;
|
||||||
@@ -35,6 +37,7 @@ public class GiftCodeResponse {
|
|||||||
this.startAt = startAt;
|
this.startAt = startAt;
|
||||||
this.endAt = endAt;
|
this.endAt = endAt;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
|
this.amount = amount;
|
||||||
}
|
}
|
||||||
// getter only
|
// getter only
|
||||||
|
|
||||||
@@ -77,4 +80,8 @@ public class GiftCodeResponse {
|
|||||||
public Instant getCreatedAt() {
|
public Instant getCreatedAt() {
|
||||||
return createdAt;
|
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 {
|
public class UserApplyGiftCodeResponse {
|
||||||
private String message;
|
private String message;
|
||||||
private String status;
|
private String status;
|
||||||
private String description;
|
private Integer amount;
|
||||||
|
private String requestId;
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse() {
|
public UserApplyGiftCodeResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse(String message, String status, String description) {
|
public UserApplyGiftCodeResponse(String message, String status, Integer amount, String requestId) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.description = description;
|
this.amount = amount;
|
||||||
|
this.requestId = requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// getters and setters
|
// getters and setters
|
||||||
@@ -31,11 +33,19 @@ public class UserApplyGiftCodeResponse {
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getRequestId() {
|
||||||
return description;
|
return requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setRequestId(String requestId) {
|
||||||
this.description = description;
|
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 {
|
public class UserConfirmGiftCodeResponse {
|
||||||
private String userId;
|
private String userId;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.dken.giftCodeService.exception;
|
package com.dken.giftcodeservice.exception;
|
||||||
|
|
||||||
public class DefaultException extends RuntimeException {
|
public class DefaultException extends RuntimeException {
|
||||||
public DefaultException(String message) {
|
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.http.HttpStatus;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.data.mongodb.config.EnableMongoAuditing;
|
import org.springframework.data.mongodb.config.EnableMongoAuditing;
|
||||||
|
import org.springframework.data.web.config.EnableSpringDataWebSupport;
|
||||||
|
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableMongoAuditing
|
@EnableMongoAuditing
|
||||||
public class giftCodeServiceApplication {
|
@org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
|
||||||
|
public class GiftCodeServiceApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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.annotation.Id;
|
||||||
import org.springframework.data.mongodb.core.mapping.Document;
|
import org.springframework.data.mongodb.core.mapping.Document;
|
||||||
@@ -26,6 +26,7 @@ public class GiftCode {
|
|||||||
private Instant startAt;
|
private Instant startAt;
|
||||||
private Instant endAt;
|
private Instant endAt;
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
private Integer amount;
|
||||||
|
|
||||||
@CreatedDate
|
@CreatedDate
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
@@ -39,7 +40,7 @@ public class GiftCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public GiftCode(String code, boolean multiUse, boolean onePerType, String type, Integer usageLimit,
|
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.code = code;
|
||||||
this.multiUse = multiUse;
|
this.multiUse = multiUse;
|
||||||
this.onePerType = onePerType;
|
this.onePerType = onePerType;
|
||||||
@@ -49,6 +50,7 @@ public class GiftCode {
|
|||||||
this.startAt = startAt;
|
this.startAt = startAt;
|
||||||
this.endAt = endAt;
|
this.endAt = endAt;
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
this.amount = amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== business methods =====
|
// ===== business methods =====
|
||||||
@@ -174,4 +176,12 @@ public class GiftCode {
|
|||||||
public void setUpdatedAt(Instant updatedAt) {
|
public void setUpdatedAt(Instant updatedAt) {
|
||||||
this.updatedAt = 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;
|
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.data.mongodb.repository.MongoRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.dken.giftCodeService.repository;
|
package com.dken.giftcodeservice.repository;
|
||||||
|
|
||||||
public interface GiftCodeRepositoryCustom {
|
public interface GiftCodeRepositoryCustom {
|
||||||
boolean increaseUsageIfAvailable(String code);
|
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.FindAndModifyOptions;
|
||||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||||
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
|
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
|
||||||
@@ -12,32 +12,40 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public class GiftCodeRepositoryImpl implements GiftCodeRepositoryCustom {
|
public class GiftCodeRepositoryImpl implements GiftCodeRepositoryCustom {
|
||||||
|
|
||||||
private final MongoTemplate mongoTemplate;
|
private final MongoTemplate mongoTemplate;
|
||||||
|
|
||||||
public GiftCodeRepositoryImpl(MongoTemplate mongoTemplate) {
|
public GiftCodeRepositoryImpl(MongoTemplate mongoTemplate) {
|
||||||
this.mongoTemplate = mongoTemplate;
|
this.mongoTemplate = mongoTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean increaseUsageIfAvailable(String code) {
|
public boolean increaseUsageIfAvailable(String code) {
|
||||||
Query query = new Query(
|
Query query = new Query(
|
||||||
new Criteria().andOperator(
|
new Criteria().andOperator(
|
||||||
Criteria.where("code").is(code),
|
Criteria.where("code").is(code),
|
||||||
Criteria.where("active").is(true),
|
Criteria.where("active").is(true),
|
||||||
new Criteria().orOperator(
|
new Criteria().orOperator(
|
||||||
Criteria.where("usageLimit").is(null),
|
Criteria.where("usageLimit").is(null),
|
||||||
Criteria.expr(
|
Criteria.expr(
|
||||||
ComparisonOperators.Lt.valueOf("usageCount")
|
ComparisonOperators.Lt
|
||||||
.lessThan("usageLimit")))));
|
.valueOf("usageCount")
|
||||||
|
.lessThan("usageLimit")))));
|
||||||
|
|
||||||
Update update = new Update().inc("usageCount", 1);
|
Update update = new Update().inc("usageCount", 1);
|
||||||
|
|
||||||
GiftCode updated = mongoTemplate.findAndModify(
|
GiftCode updated = mongoTemplate.findAndModify(
|
||||||
query,
|
query,
|
||||||
update,
|
update,
|
||||||
FindAndModifyOptions.options().returnNew(true),
|
FindAndModifyOptions.options().returnNew(true),
|
||||||
GiftCode.class);
|
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.data.mongodb.repository.MongoRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
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;
|
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;
|
|
||||||
|
|
||||||
|
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.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -20,6 +22,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -28,37 +33,35 @@ public class GiftCodeService {
|
|||||||
private final GiftCodeRepository giftCodeRepository;
|
private final GiftCodeRepository giftCodeRepository;
|
||||||
private final GiftCodeRepositoryImpl giftCodeRepositoryImpl;
|
private final GiftCodeRepositoryImpl giftCodeRepositoryImpl;
|
||||||
private final GiftCodeUsageRepository usageRepo;
|
private final GiftCodeUsageRepository usageRepo;
|
||||||
|
private final RedeemTransactionRepository redeemTransactionRepository;
|
||||||
private final RedisLockService lockService;
|
private final RedisLockService lockService;
|
||||||
|
|
||||||
public GiftCodeService(GiftCodeRepository giftCodeRepository,
|
public GiftCodeService(GiftCodeRepository giftCodeRepository,
|
||||||
GiftCodeRepositoryImpl giftCodeRepositoryImpl,
|
GiftCodeRepositoryImpl giftCodeRepositoryImpl,
|
||||||
GiftCodeUsageRepository usageRepo,
|
GiftCodeUsageRepository usageRepo,
|
||||||
|
RedeemTransactionRepository redeemTransactionRepository,
|
||||||
RedisLockService lockService) {
|
RedisLockService lockService) {
|
||||||
this.giftCodeRepository = giftCodeRepository;
|
this.giftCodeRepository = giftCodeRepository;
|
||||||
this.giftCodeRepositoryImpl = giftCodeRepositoryImpl;
|
this.giftCodeRepositoryImpl = giftCodeRepositoryImpl;
|
||||||
this.usageRepo = usageRepo;
|
this.usageRepo = usageRepo;
|
||||||
|
this.redeemTransactionRepository = redeemTransactionRepository;
|
||||||
this.lockService = lockService;
|
this.lockService = lockService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<GiftCodeResponse> getAllGiftCodes(int page, int size) {
|
public Page<GiftCodeResponse> getAllGiftCodes(int page, int size) {
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
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) {
|
private static String hashType(String type) {
|
||||||
try {
|
try {
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] hash = digest.digest(type.getBytes(StandardCharsets.UTF_8));
|
byte[] hash = digest.digest(type.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
StringBuilder hex = new StringBuilder();
|
StringBuilder hex = new StringBuilder();
|
||||||
for (byte b : hash) {
|
for (byte b : hash) {
|
||||||
hex.append(String.format("%02x", b));
|
hex.append(String.format("%02x", b));
|
||||||
}
|
}
|
||||||
return hex.toString();
|
return hex.toString();
|
||||||
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
@@ -66,21 +69,14 @@ public class GiftCodeService {
|
|||||||
|
|
||||||
String generateCode(String type) {
|
String generateCode(String type) {
|
||||||
String typeHash = hashType(type).substring(0, 6);
|
String typeHash = hashType(type).substring(0, 6);
|
||||||
String randomPart = UUID.randomUUID()
|
String randomPart = UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
|
||||||
.toString()
|
|
||||||
.replace("-", "")
|
|
||||||
.substring(0, 16)
|
|
||||||
.toUpperCase();
|
|
||||||
|
|
||||||
return (typeHash + randomPart).toUpperCase();
|
return (typeHash + randomPart).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiftCode create(GiftCodeCreateRequest req) {
|
public GiftCode create(GiftCodeCreateRequest req) {
|
||||||
if (req.getCode() == null || req.getCode().isEmpty()) {
|
if (req.getCode() == null || req.getCode().isEmpty()) {
|
||||||
String code = generateCode(req.getType());
|
req.setCode(generateCode(req.getType()));
|
||||||
req.setCode(code);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GiftCode giftCode = new GiftCode();
|
GiftCode giftCode = new GiftCode();
|
||||||
giftCode.setCode(req.getCode());
|
giftCode.setCode(req.getCode());
|
||||||
giftCode.setMultiUse(req.isMultiUse());
|
giftCode.setMultiUse(req.isMultiUse());
|
||||||
@@ -91,12 +87,12 @@ public class GiftCodeService {
|
|||||||
giftCode.setActive(req.isActive());
|
giftCode.setActive(req.isActive());
|
||||||
giftCode.setStartAt(req.getStartAt());
|
giftCode.setStartAt(req.getStartAt());
|
||||||
giftCode.setEndAt(req.getEndAt());
|
giftCode.setEndAt(req.getEndAt());
|
||||||
|
giftCode.setAmount(req.getAmount());
|
||||||
return giftCodeRepository.save(giftCode);
|
return giftCodeRepository.save(giftCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiftCodeResponse createGiftCode(GiftCodeCreateRequest req) {
|
public GiftCodeResponse createGiftCode(GiftCodeCreateRequest req) {
|
||||||
GiftCode giftCode = create(req);
|
return GiftCodeResponse.from(create(req));
|
||||||
return GiftCodeResponse.from(giftCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiftCodeResponse getGiftCodeByCode(String code) {
|
public GiftCodeResponse getGiftCodeByCode(String code) {
|
||||||
@@ -105,59 +101,98 @@ public class GiftCodeService {
|
|||||||
return GiftCodeResponse.from(giftCode);
|
return GiftCodeResponse.from(giftCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse applyGiftCode(String code, String userId) {
|
@Transactional
|
||||||
String lockKey = "lock:giftcode:" + code + ":user:" + userId;
|
public Integer reserve(GiftCodeTransactionRequest req) {
|
||||||
try {
|
Optional<RedeemTransaction> txOpt = redeemTransactionRepository.findByTransactionId(req.getTransactionId());
|
||||||
if (!lockService.lock(lockKey, java.time.Duration.ofSeconds(5))) {
|
if (txOpt.isPresent()) {
|
||||||
throw new DefaultException("Please wait and try again");
|
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"));
|
.orElseThrow(() -> new RuntimeException("Gift code not found"));
|
||||||
|
|
||||||
if (!giftCode.canUse()) {
|
if (!giftCode.canUse()) {
|
||||||
throw new DefaultException("Gift code is not available");
|
throw new DefaultException("Gift code is not available");
|
||||||
}
|
}
|
||||||
if (!giftCode.isMultiUse()) {
|
|
||||||
boolean used = usageRepo.existsByGiftCodeAndUserId(
|
|
||||||
giftCode.getCode(), userId);
|
|
||||||
|
|
||||||
if (used) {
|
checkUsageHistory(giftCode, req.getUserId());
|
||||||
throw new DefaultException("Gift code has already been used");
|
|
||||||
}
|
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,
|
RedeemTransaction tx = new RedeemTransaction(req.getTransactionId(), req.getCode(), req.getUserId(),
|
||||||
giftCode.getType(),
|
RedeemStatus.RESERVED, Instant.now());
|
||||||
giftCode.getCode());
|
redeemTransactionRepository.save(tx);
|
||||||
if (used) {
|
|
||||||
throw new DefaultException("Gift code has already been used");
|
return giftCode.getAmount();
|
||||||
}
|
|
||||||
}
|
|
||||||
effectGiftCode(giftCode, userId);
|
|
||||||
return new UserApplyGiftCodeResponse("Gift code applied successfully", "success", "");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new UserApplyGiftCodeResponse(
|
|
||||||
e.getMessage(),
|
|
||||||
"failed",
|
|
||||||
"ROLLBACK");
|
|
||||||
} finally {
|
} finally {
|
||||||
lockService.unlock(lockKey);
|
lockService.unlock(lockKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
void effectGiftCode(GiftCode giftCode, String userId) {
|
public void confirm(String transactionId) {
|
||||||
boolean increased = giftCodeRepositoryImpl
|
RedeemTransaction tx = redeemTransactionRepository.findByTransactionId(transactionId)
|
||||||
.increaseUsageIfAvailable(giftCode.getCode());
|
.orElseThrow(() -> new DefaultException("Transaction not found"));
|
||||||
|
|
||||||
if (!increased) {
|
if (tx.getStatus() == RedeemStatus.USED)
|
||||||
throw new DefaultException("Gift code is out of limit");
|
return;
|
||||||
}
|
if (tx.getStatus() == RedeemStatus.CANCELLED)
|
||||||
|
throw new DefaultException("Transaction cancelled");
|
||||||
|
|
||||||
GiftCodeUsage usage = new GiftCodeUsage(giftCode.getId(), giftCode.getCode(), giftCode.isOnePerType(),
|
tx.setStatus(RedeemStatus.USED);
|
||||||
userId);
|
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);
|
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.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
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:
|
server:
|
||||||
port: 8602
|
port: 8602
|
||||||
|
|
||||||
|
internal:
|
||||||
|
api-key: "dken-secret-key-2024"
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: gift-code-service
|
name: giftcode-service
|
||||||
data:
|
data:
|
||||||
mongodb:
|
mongodb:
|
||||||
host: localhost
|
host: mongodb
|
||||||
port: 27017
|
port: 27017
|
||||||
database: gift-codedb
|
database: giftcodedb
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: redis
|
||||||
port: 6379
|
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
|
FROM eclipse-temurin:21-jre
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /build/target/*.jar app.jar
|
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
|
@SpringBootApplication
|
||||||
@EnableMongoAuditing
|
@EnableMongoAuditing
|
||||||
|
@org.springframework.scheduling.annotation.EnableScheduling
|
||||||
public class UserServiceApplication {
|
public class UserServiceApplication {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(UserServiceApplication.class, args);
|
SpringApplication.run(UserServiceApplication.class, args);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.dken.userservice.client;
|
package com.dken.userservice.client;
|
||||||
|
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.dken.userservice.dto.response.UserApplyGiftCodeResponse;
|
import org.springframework.web.client.RestClient;
|
||||||
import com.dken.userservice.dto.request.GiftCodeValidateRequest;
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class GiftCodeClient {
|
public class GiftCodeClient {
|
||||||
@@ -15,15 +15,27 @@ public class GiftCodeClient {
|
|||||||
this.restClient = restClient;
|
this.restClient = restClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse applyGiftCode(
|
public Integer reserve(String transactionId, String userId, String code) {
|
||||||
String userId,
|
return restClient.post()
|
||||||
String giftCode) {
|
.uri("/gift-codes/reserve")
|
||||||
|
.body(Map.of("transactionId", transactionId, "userId", userId, "code", code))
|
||||||
return restClient
|
|
||||||
.post()
|
|
||||||
.uri("/gift-codes/apply")
|
|
||||||
.body(new GiftCodeValidateRequest(userId, giftCode))
|
|
||||||
.retrieve()
|
.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")
|
@Bean("giftCodeRestClient")
|
||||||
public RestClient 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()
|
return RestClient.builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
|
.defaultHeader("X-Internal-Key", apiKey)
|
||||||
.requestInterceptor((request, body, execution) -> {
|
.requestInterceptor((request, body, execution) -> {
|
||||||
return execution.execute(request, body);
|
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;
|
package com.dken.userservice.dto.request;
|
||||||
|
|
||||||
public record GiftCodeValidateRequest(
|
public record GiftCodeValidateRequest(
|
||||||
String userId,
|
String userId,
|
||||||
String giftCode) {
|
String giftCode,
|
||||||
|
String requestId) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
package com.dken.userservice.dto.response;
|
package com.dken.userservice.dto.response;
|
||||||
|
|
||||||
public class UserApplyGiftCodeResponse {
|
public class UserApplyGiftCodeResponse {
|
||||||
private String status;
|
|
||||||
private String message;
|
private String message;
|
||||||
private String description;
|
private String status;
|
||||||
|
private Integer amount;
|
||||||
|
private String requestId;
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse(String status, String message, String description) {
|
public UserApplyGiftCodeResponse() {
|
||||||
this.status = status;
|
}
|
||||||
|
|
||||||
|
public UserApplyGiftCodeResponse(String message, String status, Integer amount, String requestId) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
this.status = status;
|
||||||
|
this.amount = amount;
|
||||||
|
this.requestId = requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
@@ -27,11 +24,27 @@ public class UserApplyGiftCodeResponse {
|
|||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getStatus() {
|
||||||
return description;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public void setStatus(String status) {
|
||||||
this.description = description;
|
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 name;
|
||||||
private String email;
|
private String email;
|
||||||
private String phone;
|
private String phone;
|
||||||
|
private Long balance = 0L;
|
||||||
|
|
||||||
public String getPhone() {
|
public String getPhone() {
|
||||||
return phone;
|
return phone;
|
||||||
@@ -36,15 +37,17 @@ public class UserResponse {
|
|||||||
user.getName(),
|
user.getName(),
|
||||||
user.getEmail(),
|
user.getEmail(),
|
||||||
user.getPhone(),
|
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.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.phone = phone;
|
this.phone = phone;
|
||||||
this.sex = sex;
|
this.sex = sex;
|
||||||
|
this.balance = balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters and Setters
|
// Getters and Setters
|
||||||
@@ -71,4 +74,12 @@ public class UserResponse {
|
|||||||
public void setEmail(String email) {
|
public void setEmail(String email) {
|
||||||
this.email = 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) {
|
public Map<String, String> handleNotFound(UserNotFoundException ex) {
|
||||||
return Map.of("message", ex.getMessage());
|
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 email;
|
||||||
private String phone;
|
private String phone;
|
||||||
private UserSex sex;
|
private UserSex sex;
|
||||||
|
private Long balance = 0L;
|
||||||
|
|
||||||
private UserStatus status;
|
private UserStatus status;
|
||||||
|
|
||||||
@@ -102,6 +103,14 @@ public class User {
|
|||||||
this.sex = sex;
|
this.sex = sex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getBalance() {
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBalance(Long balance) {
|
||||||
|
this.balance = balance;
|
||||||
|
}
|
||||||
|
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
return password;
|
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.request.UserApplyGiftCodeRequest;
|
||||||
import com.dken.userservice.dto.response.UserApplyGiftCodeResponse;
|
import com.dken.userservice.dto.response.UserApplyGiftCodeResponse;
|
||||||
import com.dken.userservice.dto.response.UserResponse;
|
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.User;
|
||||||
import com.dken.userservice.model.UserSex;
|
import com.dken.userservice.model.UserSex;
|
||||||
|
import com.dken.userservice.repository.RedeemLogRepository;
|
||||||
import com.dken.userservice.repository.UserRepository;
|
import com.dken.userservice.repository.UserRepository;
|
||||||
import com.dken.userservice.exception.InvalidUserIdException;
|
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.bson.types.ObjectId;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -23,18 +31,18 @@ public class UserService {
|
|||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final GiftCodeClient giftCodeClient;
|
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.userRepository = userRepository;
|
||||||
this.giftCodeClient = giftCodeClient;
|
this.giftCodeClient = giftCodeClient;
|
||||||
|
this.redeemLogRepository = redeemLogRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<UserResponse> getAllUsers(int page, int size) {
|
public Page<UserResponse> getAllUsers(int page, int size) {
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
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) {
|
public User create(String name, String email, String password, String phone, String sex) {
|
||||||
@@ -56,12 +64,64 @@ public class UserService {
|
|||||||
if (!ObjectId.isValid(id)) {
|
if (!ObjectId.isValid(id)) {
|
||||||
throw new InvalidUserIdException(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);
|
return UserResponse.from(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserApplyGiftCodeResponse applyGiftCode(UserApplyGiftCodeRequest req) {
|
public UserApplyGiftCodeResponse applyGiftCode(UserApplyGiftCodeRequest req) {
|
||||||
User user = userRepository.findById(req.getUserId()).orElseThrow(() -> new RuntimeException("User not found"));
|
String txId = UUID.randomUUID().toString();
|
||||||
return giftCodeClient.applyGiftCode(req.getUserId(), req.getGiftCode());
|
|
||||||
|
// 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:
|
server:
|
||||||
port: 8601
|
port: 8601
|
||||||
|
|
||||||
giftcode:
|
giftcode:
|
||||||
service:
|
service:
|
||||||
url: http://localhost:8602
|
url: http://giftcode-service:8602
|
||||||
|
|
||||||
|
internal:
|
||||||
|
api-key: "dken-secret-key-2024"
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: user-service
|
name: user-service
|
||||||
data:
|
data:
|
||||||
mongodb:
|
mongodb:
|
||||||
host: localhost
|
uri: mongodb://mongodb:27017/userdb
|
||||||
port: 27017
|
|
||||||
database: userdb
|
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: redis
|
||||||
port: 6379
|
port: 6379
|
||||||
Reference in New Issue
Block a user