init user-service & giftcode-service

This commit is contained in:
2026-01-20 14:10:14 +07:00
commit 7afdfca230
59 changed files with 2777 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.dken.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
@SpringBootApplication
@EnableMongoAuditing
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,29 @@
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;
@Component
public class GiftCodeClient {
private final RestClient restClient;
public GiftCodeClient(@Qualifier("giftCodeRestClient") RestClient restClient) {
this.restClient = restClient;
}
public UserApplyGiftCodeResponse applyGiftCode(
String userId,
String giftCode) {
return restClient
.post()
.uri("/gift-codes/apply")
.body(new GiftCodeValidateRequest(userId, giftCode))
.retrieve()
.body(UserApplyGiftCodeResponse.class);
}
}

View File

@@ -0,0 +1,21 @@
package com.dken.userservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
@Configuration
public class GiftCodeClientConfig {
@Bean("giftCodeRestClient")
public RestClient giftCodeRestClient(
@Value("${giftcode.service.url}") String baseUrl) {
return RestClient.builder()
.baseUrl(baseUrl)
.requestInterceptor((request, body, execution) -> {
return execution.execute(request, body);
})
.build();
}
}

View File

@@ -0,0 +1,22 @@
package com.dken.userservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}

View File

@@ -0,0 +1,43 @@
package com.dken.userservice.controller;
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.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public UserResponse create(@Valid @RequestBody CreateUserRequest req) {
return userService.createUser(req);
}
@GetMapping("/{id}")
public UserResponse getUserById(@PathVariable String id) {
return userService.getUserById(id);
}
@GetMapping
public Page<UserResponse> getAllUsers(@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size) {
return userService.getAllUsers(page, size);
}
@PostMapping("/apply-gift-code")
public UserApplyGiftCodeResponse applyGiftCode(@Valid @RequestBody UserApplyGiftCodeRequest req) {
return userService.applyGiftCode(req);
}
}

View File

@@ -0,0 +1,61 @@
package com.dken.userservice.dto.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class CreateUserRequest {
@NotBlank(message = "Name is required")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is required")
private String password;
private String phone;
private String sex;
// Constructors
public CreateUserRequest() {
}
public CreateUserRequest(String name, String email, String password, String phone, String sex) {
this.name = name;
this.email = email;
this.password = password;
this.phone = phone;
this.sex = sex;
}
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public String getSex() {
return sex;
}
public String getPassword() {
return password;
}
}

View File

@@ -0,0 +1,6 @@
package com.dken.userservice.dto.request;
public record GiftCodeValidateRequest(
String userId,
String giftCode) {
}

View File

@@ -0,0 +1,27 @@
package com.dken.userservice.dto.request;
public class UserApplyGiftCodeRequest {
private String userId;
private String giftCode;
public UserApplyGiftCodeRequest(String userId, String giftCode) {
this.userId = userId;
this.giftCode = giftCode;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getGiftCode() {
return giftCode;
}
public void setGiftCode(String giftCode) {
this.giftCode = giftCode;
}
}

View File

@@ -0,0 +1,37 @@
package com.dken.userservice.dto.response;
public class UserApplyGiftCodeResponse {
private String status;
private String message;
private String description;
public UserApplyGiftCodeResponse(String status, String message, String description) {
this.status = status;
this.message = message;
this.description = description;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -0,0 +1,74 @@
package com.dken.userservice.dto.response;
import com.dken.userservice.model.User;
public class UserResponse {
private String id;
private String name;
private String email;
private String phone;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
private String sex;
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
// Constructors
public UserResponse() {
}
public static UserResponse from(User user) {
return new UserResponse(
user.getId(),
user.getName(),
user.getEmail(),
user.getPhone(),
user.getSex() != null ? user.getSex().name() : null);
}
public UserResponse(String id, String name, String email, String phone, String sex) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.sex = sex;
}
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

View File

@@ -0,0 +1,7 @@
package com.dken.userservice.exception;
public class DefaultException extends RuntimeException {
public DefaultException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
package com.dken.userservice.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DefaultException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleDefaultException(DefaultException ex) {
return Map.of("message", ex.getMessage());
}
@ExceptionHandler(InvalidUserIdException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleInvalidId(InvalidUserIdException ex) {
return Map.of("message", ex.getMessage());
}
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleNotFound(UserNotFoundException ex) {
return Map.of("message", ex.getMessage());
}
}

View File

@@ -0,0 +1,7 @@
package com.dken.userservice.exception;
public class InvalidEnumException extends RuntimeException {
public InvalidEnumException(String value) {
super("Invalid Enum value: " + value);
}
}

View File

@@ -0,0 +1,7 @@
package com.dken.userservice.exception;
public class InvalidUserIdException extends RuntimeException {
public InvalidUserIdException(String id) {
super("Invalid user id: " + id);
}
}

View File

@@ -0,0 +1,7 @@
package com.dken.userservice.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String id) {
super("User not found with id: " + id);
}
}

View File

@@ -0,0 +1,112 @@
package com.dken.userservice.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.Instant;
@Document(collection = "users")
public class User {
@Id
private String id;
private String name;
private String password;
private String email;
private String phone;
private UserSex sex;
private UserStatus status;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
// ===== constructors =====
public User() {
}
public User(String name, String email, String phone, UserSex sex) {
this.name = name;
this.email = email;
this.phone = phone;
this.sex = sex;
this.status = UserStatus.ACTIVE;
}
public boolean confirmGiftCode(String giftCode) {
// this.giftCode = giftCode;
return true;
}
// ===== getter & setter =====
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public UserStatus getStatus() {
return status;
}
public void setStatus(UserStatus status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public UserSex getSex() {
return sex;
}
public void setSex(UserSex sex) {
this.sex = sex;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,22 @@
package com.dken.userservice.model;
import com.dken.userservice.exception.InvalidEnumException;
import com.fasterxml.jackson.annotation.JsonCreator;
public enum UserSex {
MALE,
FEMALE,
OTHER;
@JsonCreator
public static UserSex from(String value) {
if (value == null) {
return null;
}
try {
return UserSex.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new InvalidEnumException("UserSex=" + value + " is invalid");
}
}
}

View File

@@ -0,0 +1,7 @@
package com.dken.userservice.model;
public enum UserStatus {
ACTIVE,
INACTIVE,
BLOCKED
}

View File

@@ -0,0 +1,10 @@
package com.dken.userservice.repository;
import com.dken.userservice.model.User;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository
extends MongoRepository<User, String> {
}

View File

@@ -0,0 +1,26 @@
package com.dken.userservice.service;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class RedisLockService {
private final RedisTemplate<String, String> redis;
public RedisLockService(RedisTemplate<String, String> redis) {
this.redis = redis;
}
public boolean lock(String key, Duration ttl) {
return Boolean.TRUE.equals(
redis.opsForValue()
.setIfAbsent(key, "1", ttl));
}
public void unlock(String key) {
redis.delete(key);
}
}

View File

@@ -0,0 +1,73 @@
package com.dken.userservice.service;
import com.dken.userservice.client.GiftCodeClient;
import com.dken.userservice.dto.request.CreateUserRequest;
import com.dken.userservice.dto.request.GiftCodeValidateRequest;
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.User;
import com.dken.userservice.model.UserSex;
import com.dken.userservice.repository.UserRepository;
import com.dken.userservice.exception.DefaultException;
import com.dken.userservice.exception.InvalidUserIdException;
import java.time.Instant;
import java.util.List;
import org.bson.types.ObjectId;
import java.util.stream.Collectors;
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.data.domain.Pageable;
@Service
public class UserService {
private final UserRepository userRepository;
private final GiftCodeClient giftCodeClient;
public UserService(UserRepository userRepository, GiftCodeClient giftCodeClient) {
this.userRepository = userRepository;
this.giftCodeClient = giftCodeClient;
}
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);
}
public User create(String name, String email, String password, String phone, String sex) {
User user = new User();
user.setName(name);
user.setEmail(email);
user.setPassword(password);
user.setPhone(phone);
user.setSex(UserSex.from(sex));
return userRepository.save(user);
}
public UserResponse createUser(CreateUserRequest req) {
User user = create(req.getName(), req.getEmail(), req.getPassword(), req.getPhone(), req.getSex());
return UserResponse.from(user);
}
public UserResponse getUserById(String id) {
if (!ObjectId.isValid(id)) {
throw new InvalidUserIdException(id);
}
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
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());
}
}

View File

@@ -0,0 +1 @@
spring.application.name=demo

View File

@@ -0,0 +1,15 @@
server:
port: 8601
giftcode:
service:
url: http://localhost:8602
spring:
application:
name: user-service
data:
mongodb:
uri: mongodb://localhost:27017/userdb
auto-index-creation: true
redis:
host: localhost
port: 6379

View File

@@ -0,0 +1,13 @@
package com.dken.userservice;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class UserServiceApplicationTests {
@Test
void contextLoads() {
}
}