From e65df08d1b1eb7d5daa4a6a6cb8bfebf1854ac19 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Sun, 12 Apr 2026 05:32:31 +0800 Subject: [PATCH] feat: implement User and UserCredential models with repository and service layers --- .../deltaforceguide/domain/entity/User.java | 73 +++++++++++++ .../domain/entity/UserCredential.java | 83 +++++++++++++++ .../domain/entity/UserCredentialId.java | 49 +++++++++ .../manager/UserCredentialManager.java | 47 ++++++++ .../deltaforceguide/manager/UserManager.java | 50 +++++++++ .../repository/UserCredentialRepository.java | 52 +++++++++ .../repository/UserRepository.java | 29 +++++ .../deltaforceguide/service/UserService.java | 100 ++++++++++++++++++ src/main/resources/db/migration/V4__user.sql | 16 +++ 9 files changed, 499 insertions(+) create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/repository/UserRepository.java create mode 100644 src/main/java/com/onixbyte/deltaforceguide/service/UserService.java create mode 100644 src/main/resources/db/migration/V4__user.sql diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java new file mode 100644 index 0000000..31cf527 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/User.java @@ -0,0 +1,73 @@ +package com.onixbyte.deltaforceguide.domain.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "app_user") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "username", nullable = false) + private String username; + + @Column(name = "email", nullable = false) + private String email; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List credentials = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getCredentials() { + return credentials; + } + + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + public void addCredential(UserCredential credential) { + this.credentials.add(credential); + credential.setUser(this); + } + + public void removeCredential(UserCredential credential) { + this.credentials.remove(credential); + credential.setUser(null); + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java new file mode 100644 index 0000000..44e409e --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredential.java @@ -0,0 +1,83 @@ +package com.onixbyte.deltaforceguide.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "app_user_credential") +public class UserCredential { + + @EmbeddedId + @AttributeOverrides({ + @AttributeOverride(name = "userId", column = @Column(name = "user_id")), + @AttributeOverride(name = "provider", column = @Column(name = "provider")) + }) + private UserCredentialId id = new UserCredentialId(); + + @MapsId("userId") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_credential_user")) + private User user; + + @Column(name = "credential", nullable = false, length = 255) + private String credential; + + public UserCredentialId getId() { + return id; + } + + public void setId(UserCredentialId id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + if (this.id == null) { + this.id = new UserCredentialId(); + } + this.id.setUserId(user == null ? null : user.getId()); + } + + public Long getUserId() { + return id == null ? null : id.getUserId(); + } + + public void setUserId(Long userId) { + if (this.id == null) { + this.id = new UserCredentialId(); + } + this.id.setUserId(userId); + } + + public String getProvider() { + return id == null ? null : id.getProvider(); + } + + public void setProvider(String provider) { + if (this.id == null) { + this.id = new UserCredentialId(); + } + this.id.setProvider(provider); + } + + public String getCredential() { + return credential; + } + + public void setCredential(String credential) { + this.credential = credential; + } +} diff --git a/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java new file mode 100644 index 0000000..ac1a98c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/domain/entity/UserCredentialId.java @@ -0,0 +1,49 @@ +package com.onixbyte.deltaforceguide.domain.entity; + +import jakarta.persistence.Embeddable; + +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class UserCredentialId implements Serializable { + + private Long userId; + + private String provider; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserCredentialId that = (UserCredentialId) o; + return Objects.equals(userId, that.userId) && Objects.equals(provider, that.provider); + } + + @Override + public int hashCode() { + return Objects.hash(userId, provider); + } +} + + diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java new file mode 100644 index 0000000..f4aa494 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/UserCredentialManager.java @@ -0,0 +1,47 @@ +package com.onixbyte.deltaforceguide.manager; + +import com.onixbyte.deltaforceguide.domain.entity.UserCredential; +import com.onixbyte.deltaforceguide.repository.UserCredentialRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Component +public class UserCredentialManager { + + private final UserCredentialRepository userCredentialRepository; + + public UserCredentialManager(UserCredentialRepository userCredentialRepository) { + this.userCredentialRepository = userCredentialRepository; + } + + @Transactional(readOnly = true) + public List findAllByUserId(Long userId) { + return userCredentialRepository.findAllByUserId(userId); + } + + @Transactional(readOnly = true) + public Optional findByUserIdAndProvider(Long userId, String provider) { + return userCredentialRepository.findByUserIdAndProvider(userId, provider); + } + + @Transactional + public UserCredential save(UserCredential userCredential) { + return userCredentialRepository.save(userCredential); + } + + @Transactional + public void deleteByUserIdAndProvider(Long userId, String provider) { + userCredentialRepository.deleteByUserIdAndProvider(userId, provider); + } + + @Transactional + public void deleteAllByUserId(Long userId) { + userCredentialRepository.deleteAllByUserId(userId); + } +} + + + diff --git a/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java new file mode 100644 index 0000000..08ffa3c --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/manager/UserManager.java @@ -0,0 +1,50 @@ +package com.onixbyte.deltaforceguide.manager; + +import com.onixbyte.deltaforceguide.domain.entity.User; +import com.onixbyte.deltaforceguide.repository.UserRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Component +public class UserManager { + + private final UserRepository userRepository; + + public UserManager(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Transactional(readOnly = true) + public Optional findById(Long id) { + return userRepository.findById(id); + } + + @Transactional(readOnly = true) + public List findAll() { + return userRepository.findAll(); + } + + @Transactional(readOnly = true) + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + @Transactional(readOnly = true) + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + @Transactional + public User save(User user) { + return userRepository.save(user); + } + + @Transactional + public void deleteById(Long id) { + userRepository.deleteById(id); + } +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java new file mode 100644 index 0000000..94fb8ec --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/UserCredentialRepository.java @@ -0,0 +1,52 @@ +package com.onixbyte.deltaforceguide.repository; + +import com.onixbyte.deltaforceguide.domain.entity.UserCredential; +import com.onixbyte.deltaforceguide.domain.entity.UserCredentialId; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserCredentialRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"user"}) + @Query(""" + select uc + from UserCredential uc + where uc.user.id = :userId + """) + List findAllByUserId(@Param("userId") Long userId); + + @EntityGraph(attributePaths = {"user"}) + @Query(""" + select uc + from UserCredential uc + where uc.user.id = :userId + and uc.id.provider = :provider + """) + Optional findByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider); + + @Modifying + @Query(""" + delete from UserCredential uc + where uc.user.id = :userId + and uc.id.provider = :provider + """) + void deleteByUserIdAndProvider(@Param("userId") Long userId, @Param("provider") String provider); + + @Modifying + @Query(""" + delete from UserCredential uc + where uc.user.id = :userId + """) + void deleteAllByUserId(@Param("userId") Long userId); +} + + + diff --git a/src/main/java/com/onixbyte/deltaforceguide/repository/UserRepository.java b/src/main/java/com/onixbyte/deltaforceguide/repository/UserRepository.java new file mode 100644 index 0000000..3efa050 --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/repository/UserRepository.java @@ -0,0 +1,29 @@ +package com.onixbyte.deltaforceguide.repository; + +import com.onixbyte.deltaforceguide.domain.entity.User; +import org.jspecify.annotations.NonNull; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + @Override + @EntityGraph(attributePaths = {"credentials"}) + @NonNull + Optional findById(@NonNull Long id); + + @EntityGraph(attributePaths = {"credentials"}) + Optional findByUsername(String username); + + @EntityGraph(attributePaths = {"credentials"}) + Optional findByEmail(String email); + + boolean existsByUsername(String username); + + boolean existsByEmail(String email); +} + diff --git a/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java b/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java new file mode 100644 index 0000000..c57ce4f --- /dev/null +++ b/src/main/java/com/onixbyte/deltaforceguide/service/UserService.java @@ -0,0 +1,100 @@ +package com.onixbyte.deltaforceguide.service; + +import com.onixbyte.deltaforceguide.domain.entity.User; +import com.onixbyte.deltaforceguide.domain.entity.UserCredential; +import com.onixbyte.deltaforceguide.manager.UserCredentialManager; +import com.onixbyte.deltaforceguide.manager.UserManager; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Service +public class UserService { + + private final UserManager userManager; + private final UserCredentialManager userCredentialManager; + + public UserService(UserManager userManager, UserCredentialManager userCredentialManager) { + this.userManager = userManager; + this.userCredentialManager = userCredentialManager; + } + + @Transactional(readOnly = true) + public List findAll() { + return userManager.findAll(); + } + + @Transactional(readOnly = true) + public User queryById(Long id) { + return userManager.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + id)); + } + + @Transactional(readOnly = true) + public User queryByUsername(String username) { + return userManager.findByUsername(username) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + username)); + } + + @Transactional + public User create(User user) { + return userManager.save(user); + } + + @Transactional + public User update(User user) { + if (user.getId() == null || userManager.findById(user.getId()).isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + user.getId()); + } + return userManager.save(user); + } + + @Transactional(readOnly = true) + public List findCredentials(Long userId) { + ensureUserExists(userId); + return userCredentialManager.findAllByUserId(userId); + } + + @Transactional(readOnly = true) + public UserCredential queryCredential(Long userId, String provider) { + ensureUserExists(userId); + return userCredentialManager.findByUserIdAndProvider(userId, provider) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.NOT_FOUND, + "User credential not found: userId=" + userId + ", provider=" + provider)); + } + + @Transactional + public UserCredential upsertCredential(Long userId, String provider, String credential) { + User user = ensureUserExists(userId); + UserCredential userCredential = userCredentialManager.findByUserIdAndProvider(userId, provider) + .orElseGet(UserCredential::new); + userCredential.setUser(user); + userCredential.setProvider(provider); + userCredential.setCredential(credential); + return userCredentialManager.save(userCredential); + } + + @Transactional + public void deleteCredential(Long userId, String provider) { + ensureUserExists(userId); + userCredentialManager.deleteByUserIdAndProvider(userId, provider); + } + + @Transactional + public void deleteById(Long id) { + ensureUserExists(id); + userCredentialManager.deleteAllByUserId(id); + userManager.deleteById(id); + } + + private User ensureUserExists(Long userId) { + return userManager.findById(userId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + userId)); + } +} + + diff --git a/src/main/resources/db/migration/V4__user.sql b/src/main/resources/db/migration/V4__user.sql new file mode 100644 index 0000000..f3df299 --- /dev/null +++ b/src/main/resources/db/migration/V4__user.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS app_user; +CREATE TABLE app_user +( + id BIGSERIAL NOT NULL PRIMARY KEY, + username VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL +); + +DROP TABLE IF EXISTS app_user_credential; +CREATE TABLE app_user_credential +( + user_id BIGINT NOT NULL REFERENCES app_user (id), + provider VARCHAR(255) NOT NULL, + credential VARCHAR(255) NOT NULL, + CONSTRAINT app_user_credential_pkey PRIMARY KEY (user_id, provider) +); \ No newline at end of file