QPAT commited on
Commit
30ef0a1
·
1 Parent(s): 68ce252

Optimize Performance

Browse files
Files changed (33) hide show
  1. .env +3 -0
  2. src/main/java/com/darkfantasy/config/AsyncConfig.java +41 -0
  3. src/main/java/com/darkfantasy/config/CacheConfig.java +9 -0
  4. src/main/java/com/darkfantasy/controller/UserController.java +1 -7
  5. src/main/java/com/darkfantasy/dto/article/ArticleResponse.java +9 -11
  6. src/main/java/com/darkfantasy/entity/Article.java +5 -1
  7. src/main/java/com/darkfantasy/entity/User.java +6 -2
  8. src/main/java/com/darkfantasy/entity/World.java +14 -1
  9. src/main/java/com/darkfantasy/repository/ArticleRepository.java +12 -0
  10. src/main/java/com/darkfantasy/repository/AuditLogRepository.java +4 -0
  11. src/main/java/com/darkfantasy/repository/ContactMessageRepository.java +6 -0
  12. src/main/java/com/darkfantasy/repository/ContributorRepository.java +5 -2
  13. src/main/java/com/darkfantasy/repository/FaqRepository.java +13 -1
  14. src/main/java/com/darkfantasy/repository/StoryRepository.java +6 -0
  15. src/main/java/com/darkfantasy/repository/WorldRepository.java +10 -0
  16. src/main/java/com/darkfantasy/security/CustomUserDetails.java +50 -0
  17. src/main/java/com/darkfantasy/security/ForceChangePasswordInterceptor.java +3 -0
  18. src/main/java/com/darkfantasy/service/AuditLogService.java +0 -5
  19. src/main/java/com/darkfantasy/service/CustomUserDetailsService.java +2 -6
  20. src/main/java/com/darkfantasy/service/EmailService.java +5 -0
  21. src/main/java/com/darkfantasy/service/UserService.java +2 -0
  22. src/main/java/com/darkfantasy/service/impl/ArticleServiceImpl.java +10 -15
  23. src/main/java/com/darkfantasy/service/impl/AuditLogServiceImpl.java +4 -29
  24. src/main/java/com/darkfantasy/service/impl/ContactMessageServiceImpl.java +22 -10
  25. src/main/java/com/darkfantasy/service/impl/ContributorServiceImpl.java +8 -16
  26. src/main/java/com/darkfantasy/service/impl/EmailServiceImpl.java +27 -0
  27. src/main/java/com/darkfantasy/service/impl/FaqServiceImpl.java +20 -17
  28. src/main/java/com/darkfantasy/service/impl/GameCharacterServiceImpl.java +17 -14
  29. src/main/java/com/darkfantasy/service/impl/StoryServiceImpl.java +17 -13
  30. src/main/java/com/darkfantasy/service/impl/UserServiceImpl.java +45 -34
  31. src/main/java/com/darkfantasy/service/impl/WorldServiceImpl.java +28 -17
  32. src/main/java/com/darkfantasy/util/SecurityUtil.java +19 -27
  33. src/main/resources/application.properties +34 -2
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ SPRING_DATASOURCE_URL=jdbc:mariadb://localhost:3306/MOONBLIGHT
2
+ SPRING_DATASOURCE_USERNAME=root
3
+ SPRING_DATASOURCE_PASSWORD=QQQewqwqa401!
src/main/java/com/darkfantasy/config/AsyncConfig.java ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.config;
2
+
3
+ import java.util.concurrent.Executor;
4
+ import java.util.concurrent.ThreadPoolExecutor;
5
+
6
+ import org.springframework.context.annotation.Bean;
7
+ import org.springframework.context.annotation.Configuration;
8
+ import org.springframework.scheduling.annotation.EnableAsync;
9
+ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
10
+
11
+ @Configuration
12
+ @EnableAsync
13
+ public class AsyncConfig {
14
+ @Bean(name = "taskExecutor")
15
+ public Executor taskExecutor() {
16
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17
+
18
+ // 1. Số lượng luồng CƠ BẢN luôn được duy trì sẵn sàng (như nhân viên trực ca)
19
+ executor.setCorePoolSize(5);
20
+
21
+ // 2. Kích thước HÀNG ĐỢI. Nếu 5 luồng trên đều bận, các task mới sẽ vào đây xếp
22
+ // hàng
23
+ executor.setQueueCapacity(500);
24
+
25
+ // 3. Số lượng luồng TỐI ĐA. Nếu 500 chỗ xếp hàng đều chật kín, hệ thống mới gọi
26
+ // thêm "nhân viên thời vụ" (tối đa lên 20 luồng)
27
+ executor.setMaxPoolSize(20);
28
+
29
+ // Đặt tên cho luồng để sau này log ra nhìn phát biết ngay
30
+ executor.setThreadNamePrefix("BlightMoon-Async-");
31
+
32
+ // 4. CHÍNH SÁCH XỬ LÝ QUÁ TẢI (Cực kỳ quan trọng)
33
+ // Nếu cả 20 luồng đều bận và 500 chỗ xếp hàng đều đầy thì làm gì?
34
+ // CallerRunsPolicy: Bắt chính cái luồng gọi (ví dụ luồng đang xử lý API) tự đi
35
+ // mà thực thi task đó luôn, tuyệt đối không quăng lỗi làm crash ứng dụng.
36
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
37
+
38
+ executor.initialize();
39
+ return executor;
40
+ }
41
+ }
src/main/java/com/darkfantasy/config/CacheConfig.java ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.config;
2
+
3
+ import org.springframework.cache.annotation.EnableCaching;
4
+ import org.springframework.context.annotation.Configuration;
5
+
6
+ @Configuration
7
+ @EnableCaching
8
+ public class CacheConfig {
9
+ }
src/main/java/com/darkfantasy/controller/UserController.java CHANGED
@@ -102,13 +102,7 @@ public class UserController {
102
  HttpServletResponse response,
103
  Authentication authentication) {
104
  if (authentication != null) {
105
- String username = authentication.getName();
106
- UserResponse user = userService.findByUsername(username);
107
- auditLogService.log(
108
- LogEntityType.USER,
109
- user.getId(),
110
- LogAction.LOGOUT,
111
- "Đăng xuất");
112
  }
113
 
114
  new SecurityContextLogoutHandler()
 
102
  HttpServletResponse response,
103
  Authentication authentication) {
104
  if (authentication != null) {
105
+ userService.logLogout();
 
 
 
 
 
 
106
  }
107
 
108
  new SecurityContextLogoutHandler()
src/main/java/com/darkfantasy/dto/article/ArticleResponse.java CHANGED
@@ -1,8 +1,9 @@
1
  package com.darkfantasy.dto.article;
2
 
 
 
3
  import com.darkfantasy.entity.Article;
4
  import com.darkfantasy.entity.User;
5
- import com.darkfantasy.entity.enums.ArticleType;
6
  import com.darkfantasy.util.TimeUtil;
7
 
8
  import lombok.AllArgsConstructor;
@@ -19,6 +20,7 @@ public class ArticleResponse {
19
  private Long id;
20
  private String title;
21
  private String content;
 
22
  private String authorUsername;
23
  private String thumbnailUrl;
24
  private String type;
@@ -33,26 +35,22 @@ public class ArticleResponse {
33
  .id(article.getId())
34
  .title(article.getTitle())
35
  .content(article.getContent())
 
36
  .authorUsername(article.getCreatedBy().getUsername())
37
  .thumbnailUrl(article.getThumbnailUrl())
38
  .type(article.getType().getDisplayName())
39
- .createdByUserName(article.getCreatedBy()!= null? article.getCreatedBy().getUsername() : null)
40
  .createdAt(TimeUtil.formatInstant(article.getCreatedAt()))
41
- .updatedByUserName(article.getUpdatedBy()!= null? article.getUpdatedBy().getUsername() : null)
42
  .updatedAt(TimeUtil.formatInstant(article.getUpdatedAt()))
43
  .deleted(article.isDeleted())
44
  .build();
45
  }
46
 
47
- public String getSummary() {
48
- if (content == null) {
49
  return "";
50
  }
51
-
52
- if (content.length() <= 150) {
53
- return content;
54
- }
55
-
56
- return content.substring(0, 150) + "...";
57
  }
58
  }
 
1
  package com.darkfantasy.dto.article;
2
 
3
+ import java.util.Optional;
4
+
5
  import com.darkfantasy.entity.Article;
6
  import com.darkfantasy.entity.User;
 
7
  import com.darkfantasy.util.TimeUtil;
8
 
9
  import lombok.AllArgsConstructor;
 
20
  private Long id;
21
  private String title;
22
  private String content;
23
+ private String summary;
24
  private String authorUsername;
25
  private String thumbnailUrl;
26
  private String type;
 
35
  .id(article.getId())
36
  .title(article.getTitle())
37
  .content(article.getContent())
38
+ .summary(generateSummary(article.getContent()))
39
  .authorUsername(article.getCreatedBy().getUsername())
40
  .thumbnailUrl(article.getThumbnailUrl())
41
  .type(article.getType().getDisplayName())
42
+ .createdByUserName(article.getCreatedBy() != null ? article.getCreatedBy().getUsername() : null)
43
  .createdAt(TimeUtil.formatInstant(article.getCreatedAt()))
44
+ .updatedByUserName(article.getUpdatedBy() != null ? article.getUpdatedBy().getUsername() : null)
45
  .updatedAt(TimeUtil.formatInstant(article.getUpdatedAt()))
46
  .deleted(article.isDeleted())
47
  .build();
48
  }
49
 
50
+ private static String generateSummary(String content) {
51
+ if (content == null || content.isBlank()) {
52
  return "";
53
  }
54
+ return content.length() <= 150 ? content : content.substring(0, 150) + "...";
 
 
 
 
 
55
  }
56
  }
src/main/java/com/darkfantasy/entity/Article.java CHANGED
@@ -13,6 +13,7 @@ import jakarta.persistence.FetchType;
13
  import jakarta.persistence.GeneratedValue;
14
  import jakarta.persistence.GenerationType;
15
  import jakarta.persistence.Id;
 
16
  import jakarta.persistence.JoinColumn;
17
  import jakarta.persistence.ManyToOne;
18
  import jakarta.persistence.Table;
@@ -23,7 +24,10 @@ import lombok.NoArgsConstructor;
23
  import lombok.Setter;
24
 
25
  @Entity
26
- @Table(name = "articles")
 
 
 
27
  @Getter
28
  @Setter
29
  @NoArgsConstructor
 
13
  import jakarta.persistence.GeneratedValue;
14
  import jakarta.persistence.GenerationType;
15
  import jakarta.persistence.Id;
16
+ import jakarta.persistence.Index;
17
  import jakarta.persistence.JoinColumn;
18
  import jakarta.persistence.ManyToOne;
19
  import jakarta.persistence.Table;
 
24
  import lombok.Setter;
25
 
26
  @Entity
27
+ @Table(name = "articles", indexes = {
28
+ @Index(name = "idx_article_deleted", columnList = "deleted"),
29
+ @Index(name = "idx_article_created_at", columnList = "created_at")
30
+ })
31
  @Getter
32
  @Setter
33
  @NoArgsConstructor
src/main/java/com/darkfantasy/entity/User.java CHANGED
@@ -36,6 +36,7 @@ import jakarta.persistence.Entity;
36
  import jakarta.persistence.GeneratedValue;
37
  import jakarta.persistence.GenerationType;
38
  import jakarta.persistence.Id;
 
39
  import jakarta.persistence.Table;
40
  import lombok.AllArgsConstructor;
41
  import lombok.Builder;
@@ -44,7 +45,10 @@ import lombok.NoArgsConstructor;
44
  import lombok.Setter;
45
 
46
  @Entity
47
- @Table(name = "users")
 
 
 
48
  @Getter
49
  @Setter
50
  @NoArgsConstructor
@@ -74,7 +78,7 @@ public class User {
74
  @UpdateTimestamp
75
  @Column(name = "updated_at", nullable = false)
76
  private Instant updatedAt;
77
-
78
  @Column(name = "must_change_password", nullable = false)
79
  @Builder.Default
80
  private boolean mustChangePassword = false;
 
36
  import jakarta.persistence.GeneratedValue;
37
  import jakarta.persistence.GenerationType;
38
  import jakarta.persistence.Id;
39
+ import jakarta.persistence.Index;
40
  import jakarta.persistence.Table;
41
  import lombok.AllArgsConstructor;
42
  import lombok.Builder;
 
45
  import lombok.Setter;
46
 
47
  @Entity
48
+ @Table(name = "users", indexes = {
49
+ @Index(name = "idx_user_email", columnList = "email", unique = true),
50
+ @Index(name = "idx_user_username", columnList = "username", unique = true)
51
+ })
52
  @Getter
53
  @Setter
54
  @NoArgsConstructor
 
78
  @UpdateTimestamp
79
  @Column(name = "updated_at", nullable = false)
80
  private Instant updatedAt;
81
+
82
  @Column(name = "must_change_password", nullable = false)
83
  @Builder.Default
84
  private boolean mustChangePassword = false;
src/main/java/com/darkfantasy/entity/World.java CHANGED
@@ -11,6 +11,7 @@ import jakarta.persistence.FetchType;
11
  import jakarta.persistence.GeneratedValue;
12
  import jakarta.persistence.GenerationType;
13
  import jakarta.persistence.Id;
 
14
  import jakarta.persistence.JoinColumn;
15
  import jakarta.persistence.ManyToOne;
16
  import jakarta.persistence.Table;
@@ -21,7 +22,19 @@ import lombok.NoArgsConstructor;
21
  import lombok.Setter;
22
 
23
  @Entity
24
- @Table(name = "worlds")
 
 
 
 
 
 
 
 
 
 
 
 
25
  @Getter
26
  @Setter
27
  @NoArgsConstructor
 
11
  import jakarta.persistence.GeneratedValue;
12
  import jakarta.persistence.GenerationType;
13
  import jakarta.persistence.Id;
14
+ import jakarta.persistence.Index;
15
  import jakarta.persistence.JoinColumn;
16
  import jakarta.persistence.ManyToOne;
17
  import jakarta.persistence.Table;
 
22
  import lombok.Setter;
23
 
24
  @Entity
25
+ @Table(
26
+ name = "worlds",
27
+ indexes = {
28
+ // 1. Index đơn cho các cột hay dùng trong mệnh đề WHERE
29
+ @Index(name = "idx_world_deleted", columnList = "deleted"),
30
+
31
+ // 2. Index cho cột hay dùng để sắp xếp (ORDER BY)
32
+ @Index(name = "idx_world_priority", columnList = "priority"),
33
+
34
+ // 3. Index kép (Composite Index):
35
+ @Index(name = "idx_world_deleted_priority", columnList = "deleted, priority")
36
+ }
37
+ )
38
  @Getter
39
  @Setter
40
  @NoArgsConstructor
src/main/java/com/darkfantasy/repository/ArticleRepository.java CHANGED
@@ -4,6 +4,7 @@ import java.util.Optional;
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
 
7
  import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
@@ -12,7 +13,18 @@ import com.darkfantasy.entity.enums.ArticleType;
12
 
13
  @Repository
14
  public interface ArticleRepository extends JpaRepository<Article, Long> {
 
 
 
 
15
  Page<Article> findByDeletedFalseOrderByCreatedAtDesc(Pageable pageable);
 
 
16
  Optional<Article> findFirstByTypeAndDeletedFalseOrderByCreatedAtDesc(ArticleType type);
 
 
17
  Optional<Article> findByIdAndDeletedFalse(Long id);
 
 
 
18
  }
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
+ import org.springframework.data.jpa.repository.EntityGraph;
8
  import org.springframework.data.jpa.repository.JpaRepository;
9
  import org.springframework.stereotype.Repository;
10
 
 
13
 
14
  @Repository
15
  public interface ArticleRepository extends JpaRepository<Article, Long> {
16
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
17
+ Page<Article> findAll(Pageable pageable);
18
+
19
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
20
  Page<Article> findByDeletedFalseOrderByCreatedAtDesc(Pageable pageable);
21
+
22
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
23
  Optional<Article> findFirstByTypeAndDeletedFalseOrderByCreatedAtDesc(ArticleType type);
24
+
25
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
26
  Optional<Article> findByIdAndDeletedFalse(Long id);
27
+
28
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
29
+ Page<Article> findByIdNotAndDeletedFalseOrderByCreatedAtDesc(Long id, Pageable pageable);
30
  }
src/main/java/com/darkfantasy/repository/AuditLogRepository.java CHANGED
@@ -2,6 +2,7 @@ package com.darkfantasy.repository;
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
 
5
  import org.springframework.data.jpa.repository.JpaRepository;
6
  import org.springframework.stereotype.Repository;
7
 
@@ -10,4 +11,7 @@ import com.darkfantasy.entity.AuditLog;
10
  @Repository
11
  public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
12
  Page<AuditLog> findAllByOrderByCreatedAtDesc(Pageable pageable);
 
 
 
13
  }
 
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
+ import org.springframework.data.jpa.repository.EntityGraph;
6
  import org.springframework.data.jpa.repository.JpaRepository;
7
  import org.springframework.stereotype.Repository;
8
 
 
11
  @Repository
12
  public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
13
  Page<AuditLog> findAllByOrderByCreatedAtDesc(Pageable pageable);
14
+
15
+ @EntityGraph(attributePaths = {"user"})
16
+ Page<AuditLog> findAll(Pageable pageable);
17
  }
src/main/java/com/darkfantasy/repository/ContactMessageRepository.java CHANGED
@@ -2,6 +2,7 @@ package com.darkfantasy.repository;
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
 
5
  import org.springframework.data.jpa.repository.JpaRepository;
6
  import org.springframework.stereotype.Repository;
7
 
@@ -10,6 +11,11 @@ import com.darkfantasy.entity.ContactMessage;
10
  @Repository
11
  public interface ContactMessageRepository extends JpaRepository<ContactMessage, Long> {
12
  Page<ContactMessage> findByProcessedFalse(Pageable pageable);
 
13
  Page<ContactMessage> findByProcessedTrue(Pageable pageable);
 
14
  Long countByProcessedFalse();
 
 
 
15
  }
 
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
+ import org.springframework.data.jpa.repository.EntityGraph;
6
  import org.springframework.data.jpa.repository.JpaRepository;
7
  import org.springframework.stereotype.Repository;
8
 
 
11
  @Repository
12
  public interface ContactMessageRepository extends JpaRepository<ContactMessage, Long> {
13
  Page<ContactMessage> findByProcessedFalse(Pageable pageable);
14
+
15
  Page<ContactMessage> findByProcessedTrue(Pageable pageable);
16
+
17
  Long countByProcessedFalse();
18
+
19
+ @EntityGraph(attributePaths = { "processedBy" })
20
+ Page<ContactMessage> findAll(Pageable pageable);
21
  }
src/main/java/com/darkfantasy/repository/ContributorRepository.java CHANGED
@@ -4,14 +4,17 @@ import java.util.Optional;
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
 
7
  import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
10
  import com.darkfantasy.entity.Contributor;
11
 
12
-
13
  @Repository
14
- public interface ContributorRepository extends JpaRepository<Contributor, Long>{
 
15
  Page<Contributor> findByDeletedFalseOrderByPriorityDesc(Pageable pageable);
 
 
16
  Optional<Contributor> findByIdAndDeletedFalse(Long id);
17
  }
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
+ import org.springframework.data.jpa.repository.EntityGraph;
8
  import org.springframework.data.jpa.repository.JpaRepository;
9
  import org.springframework.stereotype.Repository;
10
 
11
  import com.darkfantasy.entity.Contributor;
12
 
 
13
  @Repository
14
+ public interface ContributorRepository extends JpaRepository<Contributor, Long> {
15
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
16
  Page<Contributor> findByDeletedFalseOrderByPriorityDesc(Pageable pageable);
17
+
18
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
19
  Optional<Contributor> findByIdAndDeletedFalse(Long id);
20
  }
src/main/java/com/darkfantasy/repository/FaqRepository.java CHANGED
@@ -4,15 +4,27 @@ import java.util.List;
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
 
7
  import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
10
  import com.darkfantasy.entity.Faq;
11
 
12
  @Repository
13
- public interface FaqRepository extends JpaRepository<Faq, Long>{
 
14
  Page<Faq> findAllByOrderByPriorityDesc(Pageable pageable);
 
 
15
  Page<Faq> findByDeletedFalseOrderByPriorityDesc(Pageable pageable);
 
 
16
  Page<Faq> findAllByOrderByIdAsc(Pageable pageable);
 
 
17
  List<Faq> findByDeletedFalseOrderByPriorityDesc();
 
 
 
 
18
  }
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
+ import org.springframework.data.jpa.repository.EntityGraph;
8
  import org.springframework.data.jpa.repository.JpaRepository;
9
  import org.springframework.stereotype.Repository;
10
 
11
  import com.darkfantasy.entity.Faq;
12
 
13
  @Repository
14
+ public interface FaqRepository extends JpaRepository<Faq, Long> {
15
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
16
  Page<Faq> findAllByOrderByPriorityDesc(Pageable pageable);
17
+
18
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
19
  Page<Faq> findByDeletedFalseOrderByPriorityDesc(Pageable pageable);
20
+
21
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
22
  Page<Faq> findAllByOrderByIdAsc(Pageable pageable);
23
+
24
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
25
  List<Faq> findByDeletedFalseOrderByPriorityDesc();
26
+
27
+ @Override
28
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
29
+ Page<Faq> findAll(Pageable pageable);
30
  }
src/main/java/com/darkfantasy/repository/StoryRepository.java CHANGED
@@ -4,6 +4,7 @@ import java.util.List;
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
 
7
  import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
@@ -11,7 +12,12 @@ import com.darkfantasy.entity.Story;
11
 
12
  @Repository
13
  public interface StoryRepository extends JpaRepository<Story, Long> {
 
14
  Page<Story> findAllByOrderByIdAsc(Pageable pageable);
 
 
15
  List<Story> findByDeletedFalseOrderByPriorityDesc();
 
 
16
  Story findTopByDeletedFalseOrderByPriorityDesc();
17
  }
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
+ import org.springframework.data.jpa.repository.EntityGraph;
8
  import org.springframework.data.jpa.repository.JpaRepository;
9
  import org.springframework.stereotype.Repository;
10
 
 
12
 
13
  @Repository
14
  public interface StoryRepository extends JpaRepository<Story, Long> {
15
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
16
  Page<Story> findAllByOrderByIdAsc(Pageable pageable);
17
+
18
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
19
  List<Story> findByDeletedFalseOrderByPriorityDesc();
20
+
21
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
22
  Story findTopByDeletedFalseOrderByPriorityDesc();
23
  }
src/main/java/com/darkfantasy/repository/WorldRepository.java CHANGED
@@ -1,9 +1,11 @@
1
  package com.darkfantasy.repository;
2
 
3
  import java.util.List;
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
 
7
  import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
@@ -11,7 +13,15 @@ import com.darkfantasy.entity.World;
11
 
12
  @Repository
13
  public interface WorldRepository extends JpaRepository<World, Long> {
 
14
  Page<World> findAllByOrderByIdAsc(Pageable pageable);
 
 
15
  List<World> findByDeletedFalseOrderByPriorityDesc();
 
 
16
  World findTopByDeletedFalseOrderByPriorityDesc();
 
 
 
17
  }
 
1
  package com.darkfantasy.repository;
2
 
3
  import java.util.List;
4
+ import java.util.Optional;
5
 
6
  import org.springframework.data.domain.Page;
7
  import org.springframework.data.domain.Pageable;
8
+ import org.springframework.data.jpa.repository.EntityGraph;
9
  import org.springframework.data.jpa.repository.JpaRepository;
10
  import org.springframework.stereotype.Repository;
11
 
 
13
 
14
  @Repository
15
  public interface WorldRepository extends JpaRepository<World, Long> {
16
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
17
  Page<World> findAllByOrderByIdAsc(Pageable pageable);
18
+
19
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
20
  List<World> findByDeletedFalseOrderByPriorityDesc();
21
+
22
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
23
  World findTopByDeletedFalseOrderByPriorityDesc();
24
+
25
+ @EntityGraph(attributePaths = { "createdBy", "updatedBy" })
26
+ Optional<World> findById(Long id);
27
  }
src/main/java/com/darkfantasy/security/CustomUserDetails.java ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.security;
2
+
3
+ import com.darkfantasy.entity.User;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Getter;
6
+ import org.springframework.security.core.GrantedAuthority;
7
+ import org.springframework.security.core.authority.SimpleGrantedAuthority;
8
+ import org.springframework.security.core.userdetails.UserDetails;
9
+
10
+ import java.util.Collection;
11
+ import java.util.List;
12
+
13
+ @Getter
14
+ @AllArgsConstructor
15
+ public class CustomUserDetails implements UserDetails {
16
+ private Long id;
17
+ private String username;
18
+ private String password;
19
+ private boolean isActive;
20
+ private Collection<? extends GrantedAuthority> authorities;
21
+ private String role;
22
+
23
+
24
+ public static CustomUserDetails build(User user) {
25
+ List<GrantedAuthority> authorities = List.of(
26
+ new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
27
+ );
28
+
29
+ return new CustomUserDetails(
30
+ user.getId(),
31
+ user.getUsername(),
32
+ user.getPassword(),
33
+ user.isActive(),
34
+ authorities,
35
+ user.getRole().name()
36
+ );
37
+ }
38
+
39
+ @Override
40
+ public boolean isAccountNonExpired() { return true; }
41
+
42
+ @Override
43
+ public boolean isAccountNonLocked() { return true; }
44
+
45
+ @Override
46
+ public boolean isCredentialsNonExpired() { return true; }
47
+
48
+ @Override
49
+ public boolean isEnabled() { return isActive; }
50
+ }
src/main/java/com/darkfantasy/security/ForceChangePasswordInterceptor.java CHANGED
@@ -23,6 +23,9 @@ public class ForceChangePasswordInterceptor implements HandlerInterceptor {
23
  HttpServletResponse response,
24
  Object handler)
25
  throws Exception {
 
 
 
26
  String username = SecurityUtil.getCurrentUserName();
27
  if (username == null) {
28
  return true;
 
23
  HttpServletResponse response,
24
  Object handler)
25
  throws Exception {
26
+ if(!SecurityUtil.isAuthenticated()){
27
+ return true;
28
+ }
29
  String username = SecurityUtil.getCurrentUserName();
30
  if (username == null) {
31
  return true;
src/main/java/com/darkfantasy/service/AuditLogService.java CHANGED
@@ -9,11 +9,6 @@ import com.darkfantasy.entity.enums.LogAction;
9
  import com.darkfantasy.entity.enums.LogEntityType;
10
 
11
  public interface AuditLogService {
12
- void log(
13
- LogEntityType entityType,
14
- Long entityId,
15
- LogAction action,
16
- String description);
17
 
18
  void log(
19
  User user,
 
9
  import com.darkfantasy.entity.enums.LogEntityType;
10
 
11
  public interface AuditLogService {
 
 
 
 
 
12
 
13
  void log(
14
  User user,
src/main/java/com/darkfantasy/service/CustomUserDetailsService.java CHANGED
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Service;
7
 
8
  import com.darkfantasy.entity.User;
9
  import com.darkfantasy.repository.UserRepository;
 
10
 
11
  import lombok.RequiredArgsConstructor;
12
 
@@ -22,12 +23,7 @@ public class CustomUserDetailsService implements UserDetailsService {
22
  .orElseThrow(() -> new UsernameNotFoundException(
23
  "Không tìm thấy tài khoản"));
24
 
25
- return org.springframework.security.core.userdetails.User
26
- .withUsername(user.getUsername())
27
- .password(user.getPassword())
28
- .disabled(!user.isActive())
29
- .authorities("ROLE_" + user.getRole().name())
30
- .build();
31
  }
32
 
33
  }
 
7
 
8
  import com.darkfantasy.entity.User;
9
  import com.darkfantasy.repository.UserRepository;
10
+ import com.darkfantasy.security.CustomUserDetails;
11
 
12
  import lombok.RequiredArgsConstructor;
13
 
 
23
  .orElseThrow(() -> new UsernameNotFoundException(
24
  "Không tìm thấy tài khoản"));
25
 
26
+ return CustomUserDetails.build(user);
 
 
 
 
 
27
  }
28
 
29
  }
src/main/java/com/darkfantasy/service/EmailService.java ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package com.darkfantasy.service;
2
+
3
+ public interface EmailService {
4
+ void sendPasswordResetEmail(String to, String otp);
5
+ }
src/main/java/com/darkfantasy/service/UserService.java CHANGED
@@ -41,4 +41,6 @@ public interface UserService {
41
  void verifyOtp(VerifyOtpRequest request);
42
 
43
  void resetPassword(ResetPasswordRequest request);
 
 
44
  }
 
41
  void verifyOtp(VerifyOtpRequest request);
42
 
43
  void resetPassword(ResetPasswordRequest request);
44
+
45
+ void logLogout();
46
  }
src/main/java/com/darkfantasy/service/impl/ArticleServiceImpl.java CHANGED
@@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor;
26
 
27
  @Service
28
  @RequiredArgsConstructor
 
29
  public class ArticleServiceImpl implements ArticleService {
30
  private final UserRepository userRepository;
31
  private final ArticleRepository articleRepository;
@@ -47,6 +48,7 @@ public class ArticleServiceImpl implements ArticleService {
47
  article.setCreatedBy(currentUser);
48
  Article savedArticle = articleRepository.save(article);
49
  auditLogService.log(
 
50
  LogEntityType.ARTICLE,
51
  savedArticle.getId(),
52
  LogAction.CREATE,
@@ -60,12 +62,6 @@ public class ArticleServiceImpl implements ArticleService {
60
 
61
  Article article = findArticle(request.getId());
62
 
63
- String currentUsername = SecurityUtil.getCurrentUserName();
64
-
65
- if (currentUsername == null) {
66
- throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
67
- }
68
-
69
  User currentUser = getCurrentUser();
70
 
71
  article.setTitle(request.getTitle());
@@ -78,6 +74,7 @@ public class ArticleServiceImpl implements ArticleService {
78
 
79
  article.setUpdatedBy(currentUser);
80
  auditLogService.log(
 
81
  LogEntityType.ARTICLE,
82
  article.getId(),
83
  LogAction.UPDATE,
@@ -96,6 +93,7 @@ public class ArticleServiceImpl implements ArticleService {
96
  article.setDeleted(true);
97
  article.setUpdatedBy(currentUser);
98
  auditLogService.log(
 
99
  LogEntityType.ARTICLE,
100
  article.getId(),
101
  LogAction.DELETE,
@@ -147,6 +145,7 @@ public class ArticleServiceImpl implements ArticleService {
147
  article.setDeleted(false);
148
  article.setUpdatedBy(currentUser);
149
  auditLogService.log(
 
150
  LogEntityType.ARTICLE,
151
  article.getId(),
152
  LogAction.RESTORE,
@@ -155,12 +154,10 @@ public class ArticleServiceImpl implements ArticleService {
155
 
156
  @Override
157
  public List<ArticleResponse> getLatestArticlesExcept(Long id, int limit) {
158
- Pageable pageable = PageRequest.of(0, limit + 1);
159
 
160
- return articleRepository.findByDeletedFalseOrderByCreatedAtDesc(pageable)
161
  .stream()
162
- .filter(article -> !article.getId().equals(id))
163
- .limit(limit)
164
  .map(ArticleResponse::fromEntity)
165
  .toList();
166
  }
@@ -177,14 +174,12 @@ public class ArticleServiceImpl implements ArticleService {
177
 
178
  private User getCurrentUser() {
179
 
180
- String currentUsername = SecurityUtil.getCurrentUserName();
181
 
182
- if (currentUsername == null) {
183
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
184
  }
185
-
186
- return userRepository.findUserByUsername(currentUsername)
187
- .orElseThrow(() -> new ResourceNotFoundException("Không tìm thấy người dùng"));
188
  }
189
 
190
  @Override
 
26
 
27
  @Service
28
  @RequiredArgsConstructor
29
+ @Transactional(readOnly = true)
30
  public class ArticleServiceImpl implements ArticleService {
31
  private final UserRepository userRepository;
32
  private final ArticleRepository articleRepository;
 
48
  article.setCreatedBy(currentUser);
49
  Article savedArticle = articleRepository.save(article);
50
  auditLogService.log(
51
+ currentUser,
52
  LogEntityType.ARTICLE,
53
  savedArticle.getId(),
54
  LogAction.CREATE,
 
62
 
63
  Article article = findArticle(request.getId());
64
 
 
 
 
 
 
 
65
  User currentUser = getCurrentUser();
66
 
67
  article.setTitle(request.getTitle());
 
74
 
75
  article.setUpdatedBy(currentUser);
76
  auditLogService.log(
77
+ currentUser,
78
  LogEntityType.ARTICLE,
79
  article.getId(),
80
  LogAction.UPDATE,
 
93
  article.setDeleted(true);
94
  article.setUpdatedBy(currentUser);
95
  auditLogService.log(
96
+ currentUser,
97
  LogEntityType.ARTICLE,
98
  article.getId(),
99
  LogAction.DELETE,
 
145
  article.setDeleted(false);
146
  article.setUpdatedBy(currentUser);
147
  auditLogService.log(
148
+ currentUser,
149
  LogEntityType.ARTICLE,
150
  article.getId(),
151
  LogAction.RESTORE,
 
154
 
155
  @Override
156
  public List<ArticleResponse> getLatestArticlesExcept(Long id, int limit) {
157
+ Pageable pageable = PageRequest.of(0, limit);
158
 
159
+ return articleRepository.findByIdNotAndDeletedFalseOrderByCreatedAtDesc(id, pageable)
160
  .stream()
 
 
161
  .map(ArticleResponse::fromEntity)
162
  .toList();
163
  }
 
174
 
175
  private User getCurrentUser() {
176
 
177
+ Long currentUserId = SecurityUtil.getCurrentUserId();
178
 
179
+ if (currentUserId == null) {
180
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
181
  }
182
+ return userRepository.getReferenceById(currentUserId);
 
 
183
  }
184
 
185
  @Override
src/main/java/com/darkfantasy/service/impl/AuditLogServiceImpl.java CHANGED
@@ -2,6 +2,7 @@ package com.darkfantasy.service.impl;
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
 
5
  import org.springframework.stereotype.Service;
6
  import org.springframework.transaction.annotation.Transactional;
7
 
@@ -19,40 +20,12 @@ import lombok.RequiredArgsConstructor;
19
 
20
  @Service
21
  @RequiredArgsConstructor
 
22
  public class AuditLogServiceImpl implements AuditLogService {
23
 
24
  private final AuditLogRepository auditLogRepository;
25
  private final UserRepository userRepository;
26
 
27
- @Transactional
28
- @Override
29
- public void log(
30
- LogEntityType entityType,
31
- Long entityId,
32
- LogAction action,
33
- String description) {
34
-
35
- String username = SecurityUtil.getCurrentUserName();
36
-
37
- if (username == null || "anonymousUser".equals(username)) {
38
- return;
39
- }
40
-
41
- User currentUser = userRepository
42
- .findUserByUsername(username)
43
- .orElseThrow(() -> new IllegalArgumentException(
44
- "Không tìm thấy người dùng"));
45
-
46
- AuditLog log = AuditLog.builder()
47
- .user(currentUser)
48
- .entityType(entityType)
49
- .entityId(entityId)
50
- .action(action)
51
- .description(description)
52
- .build();
53
-
54
- auditLogRepository.save(log);
55
- }
56
 
57
  @Override
58
  public Page<AuditLogResponse> getLogs(Pageable pageable) {
@@ -61,6 +34,7 @@ public class AuditLogServiceImpl implements AuditLogService {
61
  .map(AuditLogResponse::fromEntity);
62
  }
63
 
 
64
  @Transactional
65
  @Override
66
  public void log(
@@ -80,4 +54,5 @@ public class AuditLogServiceImpl implements AuditLogService {
80
 
81
  auditLogRepository.save(log);
82
  }
 
83
  }
 
2
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
+ import org.springframework.scheduling.annotation.Async;
6
  import org.springframework.stereotype.Service;
7
  import org.springframework.transaction.annotation.Transactional;
8
 
 
20
 
21
  @Service
22
  @RequiredArgsConstructor
23
+ @Transactional(readOnly = true)
24
  public class AuditLogServiceImpl implements AuditLogService {
25
 
26
  private final AuditLogRepository auditLogRepository;
27
  private final UserRepository userRepository;
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  @Override
31
  public Page<AuditLogResponse> getLogs(Pageable pageable) {
 
34
  .map(AuditLogResponse::fromEntity);
35
  }
36
 
37
+ @Async
38
  @Transactional
39
  @Override
40
  public void log(
 
54
 
55
  auditLogRepository.save(log);
56
  }
57
+
58
  }
src/main/java/com/darkfantasy/service/impl/ContactMessageServiceImpl.java CHANGED
@@ -8,19 +8,24 @@ import org.springframework.transaction.annotation.Transactional;
8
  import com.darkfantasy.dto.contact.ContactMessageResponse;
9
  import com.darkfantasy.dto.contact.CreateContactMessageRequest;
10
  import com.darkfantasy.entity.ContactMessage;
 
11
  import com.darkfantasy.entity.enums.LogAction;
12
  import com.darkfantasy.entity.enums.LogEntityType;
13
  import com.darkfantasy.repository.ContactMessageRepository;
 
14
  import com.darkfantasy.service.AuditLogService;
15
  import com.darkfantasy.service.ContactMessageService;
 
16
 
17
  import lombok.RequiredArgsConstructor;
18
 
19
  @Service
20
  @RequiredArgsConstructor
 
21
  public class ContactMessageServiceImpl implements ContactMessageService {
22
  private final ContactMessageRepository contactMessageRepository;
23
  private final AuditLogService auditLogService;
 
24
 
25
  @Transactional
26
  @Override
@@ -32,11 +37,12 @@ public class ContactMessageServiceImpl implements ContactMessageService {
32
  ContactMessage message = request.toEntity();
33
  ContactMessage savedMessage = contactMessageRepository.save(message);
34
 
35
- // auditLogService.log(
36
- // LogEntityType.CONTACT,
37
- // savedMessage.getId(),
38
- // LogAction.CREATE,
39
- // "Tạo liên hệ: " + savedMessage.getMessage());
 
40
  return ContactMessageResponse.fromEntity(savedMessage);
41
  }
42
 
@@ -65,11 +71,17 @@ public class ContactMessageServiceImpl implements ContactMessageService {
65
 
66
  ContactMessage message = findContactMessage(id);
67
  message.setProcessed(true);
68
- auditLogService.log(
69
- LogEntityType.CONTACT,
70
- message.getId(),
71
- LogAction.PROCESS,
72
- "Đánh dấu đã xử lý: " + message.getMessage());
 
 
 
 
 
 
73
  }
74
 
75
  @Transactional
 
8
  import com.darkfantasy.dto.contact.ContactMessageResponse;
9
  import com.darkfantasy.dto.contact.CreateContactMessageRequest;
10
  import com.darkfantasy.entity.ContactMessage;
11
+ import com.darkfantasy.entity.User;
12
  import com.darkfantasy.entity.enums.LogAction;
13
  import com.darkfantasy.entity.enums.LogEntityType;
14
  import com.darkfantasy.repository.ContactMessageRepository;
15
+ import com.darkfantasy.repository.UserRepository;
16
  import com.darkfantasy.service.AuditLogService;
17
  import com.darkfantasy.service.ContactMessageService;
18
+ import com.darkfantasy.util.SecurityUtil;
19
 
20
  import lombok.RequiredArgsConstructor;
21
 
22
  @Service
23
  @RequiredArgsConstructor
24
+ @Transactional(readOnly = true)
25
  public class ContactMessageServiceImpl implements ContactMessageService {
26
  private final ContactMessageRepository contactMessageRepository;
27
  private final AuditLogService auditLogService;
28
+ private final UserRepository userRepository;
29
 
30
  @Transactional
31
  @Override
 
37
  ContactMessage message = request.toEntity();
38
  ContactMessage savedMessage = contactMessageRepository.save(message);
39
 
40
+ auditLogService.log(
41
+ null,
42
+ LogEntityType.CONTACT,
43
+ savedMessage.getId(),
44
+ LogAction.CREATE,
45
+ "Khách vãng lai (" + request.getVisitorEmail() + ") gửi liên hệ");
46
  return ContactMessageResponse.fromEntity(savedMessage);
47
  }
48
 
 
71
 
72
  ContactMessage message = findContactMessage(id);
73
  message.setProcessed(true);
74
+ Long currentUserId = SecurityUtil.getCurrentUserId();
75
+ if (currentUserId != null) {
76
+ User currentUser = userRepository.getReferenceById(currentUserId);
77
+ auditLogService.log(
78
+ currentUser,
79
+ LogEntityType.CONTACT,
80
+ message.getId(),
81
+ LogAction.PROCESS,
82
+ "Đánh dấu đã xử lý: " + message.getMessage());
83
+ }
84
+
85
  }
86
 
87
  @Transactional
src/main/java/com/darkfantasy/service/impl/ContributorServiceImpl.java CHANGED
@@ -1,9 +1,6 @@
1
  package com.darkfantasy.service.impl;
2
 
3
- import java.util.List;
4
-
5
  import org.springframework.data.domain.Page;
6
- import org.springframework.data.domain.PageRequest;
7
  import org.springframework.data.domain.Pageable;
8
  import org.springframework.stereotype.Service;
9
  import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +22,7 @@ import lombok.RequiredArgsConstructor;
25
 
26
  @Service
27
  @RequiredArgsConstructor
 
28
  public class ContributorServiceImpl implements ContributorService {
29
  private final UserRepository userRepository;
30
  private final ContributorRepository contributorRepository;
@@ -46,6 +44,7 @@ public class ContributorServiceImpl implements ContributorService {
46
  contributor.setCreatedBy(currentUser);
47
  Contributor savedContributor = contributorRepository.save(contributor);
48
  auditLogService.log(
 
49
  LogEntityType.CONTRIBUTOR,
50
  savedContributor.getId(),
51
  LogAction.CREATE,
@@ -59,12 +58,6 @@ public class ContributorServiceImpl implements ContributorService {
59
 
60
  Contributor contributor = findContributor(request.getId());
61
 
62
- String currentUsername = SecurityUtil.getCurrentUserName();
63
-
64
- if (currentUsername == null) {
65
- throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
66
- }
67
-
68
  User currentUser = getCurrentUser();
69
 
70
  contributor.setName(request.getName());
@@ -77,6 +70,7 @@ public class ContributorServiceImpl implements ContributorService {
77
 
78
  contributor.setUpdatedBy(currentUser);
79
  auditLogService.log(
 
80
  LogEntityType.CONTRIBUTOR,
81
  contributor.getId(),
82
  LogAction.UPDATE,
@@ -95,6 +89,7 @@ public class ContributorServiceImpl implements ContributorService {
95
  contributor.setDeleted(true);
96
  contributor.setUpdatedBy(currentUser);
97
  auditLogService.log(
 
98
  LogEntityType.CONTRIBUTOR,
99
  contributor.getId(),
100
  LogAction.DELETE,
@@ -126,6 +121,7 @@ public class ContributorServiceImpl implements ContributorService {
126
  contributor.setDeleted(false);
127
  contributor.setUpdatedBy(currentUser);
128
  auditLogService.log(
 
129
  LogEntityType.CONTRIBUTOR,
130
  contributor.getId(),
131
  LogAction.RESTORE,
@@ -143,14 +139,10 @@ public class ContributorServiceImpl implements ContributorService {
143
  }
144
 
145
  private User getCurrentUser() {
146
-
147
- String currentUsername = SecurityUtil.getCurrentUserName();
148
-
149
- if (currentUsername == null) {
150
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
151
  }
152
-
153
- return userRepository.findUserByUsername(currentUsername)
154
- .orElseThrow(() -> new ResourceNotFoundException("Không tìm thấy người dùng"));
155
  }
156
  }
 
1
  package com.darkfantasy.service.impl;
2
 
 
 
3
  import org.springframework.data.domain.Page;
 
4
  import org.springframework.data.domain.Pageable;
5
  import org.springframework.stereotype.Service;
6
  import org.springframework.transaction.annotation.Transactional;
 
22
 
23
  @Service
24
  @RequiredArgsConstructor
25
+ @Transactional(readOnly = true)
26
  public class ContributorServiceImpl implements ContributorService {
27
  private final UserRepository userRepository;
28
  private final ContributorRepository contributorRepository;
 
44
  contributor.setCreatedBy(currentUser);
45
  Contributor savedContributor = contributorRepository.save(contributor);
46
  auditLogService.log(
47
+ currentUser,
48
  LogEntityType.CONTRIBUTOR,
49
  savedContributor.getId(),
50
  LogAction.CREATE,
 
58
 
59
  Contributor contributor = findContributor(request.getId());
60
 
 
 
 
 
 
 
61
  User currentUser = getCurrentUser();
62
 
63
  contributor.setName(request.getName());
 
70
 
71
  contributor.setUpdatedBy(currentUser);
72
  auditLogService.log(
73
+ currentUser,
74
  LogEntityType.CONTRIBUTOR,
75
  contributor.getId(),
76
  LogAction.UPDATE,
 
89
  contributor.setDeleted(true);
90
  contributor.setUpdatedBy(currentUser);
91
  auditLogService.log(
92
+ currentUser,
93
  LogEntityType.CONTRIBUTOR,
94
  contributor.getId(),
95
  LogAction.DELETE,
 
121
  contributor.setDeleted(false);
122
  contributor.setUpdatedBy(currentUser);
123
  auditLogService.log(
124
+ currentUser,
125
  LogEntityType.CONTRIBUTOR,
126
  contributor.getId(),
127
  LogAction.RESTORE,
 
139
  }
140
 
141
  private User getCurrentUser() {
142
+ Long currentUserId = SecurityUtil.getCurrentUserId();
143
+ if (currentUserId == null) {
 
 
144
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
145
  }
146
+ return userRepository.getReferenceById(currentUserId);
 
 
147
  }
148
  }
src/main/java/com/darkfantasy/service/impl/EmailServiceImpl.java ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.darkfantasy.service.impl;
2
+
3
+ import org.springframework.mail.SimpleMailMessage;
4
+ import org.springframework.mail.javamail.JavaMailSender;
5
+ import org.springframework.scheduling.annotation.Async;
6
+ import org.springframework.stereotype.Service;
7
+
8
+ import com.darkfantasy.service.EmailService;
9
+
10
+ import lombok.RequiredArgsConstructor;
11
+
12
+ @Service
13
+ @RequiredArgsConstructor
14
+ public class EmailServiceImpl implements EmailService {
15
+
16
+ private final JavaMailSender mailSender;
17
+
18
+ @Async
19
+ @Override
20
+ public void sendPasswordResetEmail(String to, String otp) {
21
+ SimpleMailMessage message = new SimpleMailMessage();
22
+ message.setTo(to);
23
+ message.setSubject("Mã OTP đặt lại mật khẩu");
24
+ message.setText("Mã OTP của bạn là: " + otp + "\n\nMã có hiệu lực trong 5 phút.");
25
+ mailSender.send(message);
26
+ }
27
+ }
src/main/java/com/darkfantasy/service/impl/FaqServiceImpl.java CHANGED
@@ -24,6 +24,7 @@ import lombok.RequiredArgsConstructor;
24
 
25
  @Service
26
  @RequiredArgsConstructor
 
27
  public class FaqServiceImpl implements FaqService {
28
  private final FaqRepository faqRepository;
29
  private final UserRepository userRepository;
@@ -44,15 +45,17 @@ public class FaqServiceImpl implements FaqService {
44
  @Transactional
45
  @Override
46
  public FaqResponse createFaq(CreateFaqRequest request) {
 
47
  if (request == null) {
48
  throw new IllegalArgumentException(
49
  "Không thể tạo faq với dữ liệu null");
50
  }
51
 
52
  Faq faq = request.toEntity();
53
- faq.setCreatedBy(getCurrentUser());
54
  Faq savedFaq = faqRepository.save(faq);
55
  auditLogService.log(
 
56
  LogEntityType.FAQ,
57
  savedFaq.getId(),
58
  LogAction.CREATE,
@@ -63,6 +66,7 @@ public class FaqServiceImpl implements FaqService {
63
  @Transactional
64
  @Override
65
  public FaqResponse updateFaq(UpdateFaqRequest request) {
 
66
  if (request == null) {
67
  throw new IllegalArgumentException(
68
  "Không thể thay đổi faq với dữ liệu null");
@@ -71,8 +75,9 @@ public class FaqServiceImpl implements FaqService {
71
  faq.setTitle(request.getTitle());
72
  faq.setContent(request.getContent());
73
  faq.setPriority(request.getPriority());
74
- faq.setUpdatedBy(getCurrentUser());
75
  auditLogService.log(
 
76
  LogEntityType.FAQ,
77
  faq.getId(),
78
  LogAction.UPDATE,
@@ -83,13 +88,15 @@ public class FaqServiceImpl implements FaqService {
83
  @Transactional
84
  @Override
85
  public void deleteFaq(Long id) {
 
86
  if (id == null) {
87
  throw new IllegalArgumentException("Không thể xóa faq với ID null");
88
  }
89
  Faq found = findFaq(id);
90
  found.setDeleted(true);
91
- found.setUpdatedBy(getCurrentUser());
92
  auditLogService.log(
 
93
  LogEntityType.FAQ,
94
  found.getId(),
95
  LogAction.DELETE,
@@ -99,13 +106,15 @@ public class FaqServiceImpl implements FaqService {
99
  @Transactional
100
  @Override
101
  public void restoreFaq(Long id) {
 
102
  if (id == null) {
103
  throw new IllegalArgumentException("Không thể khôi phục faq với ID null");
104
  }
105
  Faq found = findFaq(id);
106
  found.setDeleted(false);
107
- found.setUpdatedBy(getCurrentUser());
108
  auditLogService.log(
 
109
  LogEntityType.FAQ,
110
  found.getId(),
111
  LogAction.RESTORE,
@@ -121,27 +130,21 @@ public class FaqServiceImpl implements FaqService {
121
  .toList();
122
  }
123
 
124
- private Faq findFaq(Long id) {
125
- return faqRepository.findById(id)
126
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy faq với ID: " + id));
127
- }
128
-
129
  @Override
130
  public long count() {
131
  return faqRepository.count();
132
  }
133
 
134
  private User getCurrentUser() {
135
-
136
- String currentUsername = SecurityUtil.getCurrentUserName();
137
-
138
- if (currentUsername == null) {
139
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
140
  }
141
-
142
- return userRepository
143
- .findUserByUsername(currentUsername)
144
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng"));
145
  }
146
 
 
 
 
 
147
  }
 
24
 
25
  @Service
26
  @RequiredArgsConstructor
27
+ @Transactional(readOnly = true)
28
  public class FaqServiceImpl implements FaqService {
29
  private final FaqRepository faqRepository;
30
  private final UserRepository userRepository;
 
45
  @Transactional
46
  @Override
47
  public FaqResponse createFaq(CreateFaqRequest request) {
48
+ User currentUser = getCurrentUser();
49
  if (request == null) {
50
  throw new IllegalArgumentException(
51
  "Không thể tạo faq với dữ liệu null");
52
  }
53
 
54
  Faq faq = request.toEntity();
55
+ faq.setCreatedBy(currentUser);
56
  Faq savedFaq = faqRepository.save(faq);
57
  auditLogService.log(
58
+ currentUser,
59
  LogEntityType.FAQ,
60
  savedFaq.getId(),
61
  LogAction.CREATE,
 
66
  @Transactional
67
  @Override
68
  public FaqResponse updateFaq(UpdateFaqRequest request) {
69
+ User currentUser = getCurrentUser();
70
  if (request == null) {
71
  throw new IllegalArgumentException(
72
  "Không thể thay đổi faq với dữ liệu null");
 
75
  faq.setTitle(request.getTitle());
76
  faq.setContent(request.getContent());
77
  faq.setPriority(request.getPriority());
78
+ faq.setUpdatedBy(currentUser);
79
  auditLogService.log(
80
+ currentUser,
81
  LogEntityType.FAQ,
82
  faq.getId(),
83
  LogAction.UPDATE,
 
88
  @Transactional
89
  @Override
90
  public void deleteFaq(Long id) {
91
+ User currentUser = getCurrentUser();
92
  if (id == null) {
93
  throw new IllegalArgumentException("Không thể xóa faq với ID null");
94
  }
95
  Faq found = findFaq(id);
96
  found.setDeleted(true);
97
+ found.setUpdatedBy(currentUser);
98
  auditLogService.log(
99
+ currentUser,
100
  LogEntityType.FAQ,
101
  found.getId(),
102
  LogAction.DELETE,
 
106
  @Transactional
107
  @Override
108
  public void restoreFaq(Long id) {
109
+ User currentUser = getCurrentUser();
110
  if (id == null) {
111
  throw new IllegalArgumentException("Không thể khôi phục faq với ID null");
112
  }
113
  Faq found = findFaq(id);
114
  found.setDeleted(false);
115
+ found.setUpdatedBy(currentUser);
116
  auditLogService.log(
117
+ currentUser,
118
  LogEntityType.FAQ,
119
  found.getId(),
120
  LogAction.RESTORE,
 
130
  .toList();
131
  }
132
 
 
 
 
 
 
133
  @Override
134
  public long count() {
135
  return faqRepository.count();
136
  }
137
 
138
  private User getCurrentUser() {
139
+ Long currentUserId = SecurityUtil.getCurrentUserId();
140
+ if (currentUserId == null) {
 
 
141
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
142
  }
143
+ return userRepository.getReferenceById(currentUserId);
 
 
 
144
  }
145
 
146
+ private Faq findFaq(Long id) {
147
+ return faqRepository.findById(id)
148
+ .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy faq với ID: " + id));
149
+ }
150
  }
src/main/java/com/darkfantasy/service/impl/GameCharacterServiceImpl.java CHANGED
@@ -25,6 +25,7 @@ import lombok.RequiredArgsConstructor;
25
 
26
  @Service
27
  @RequiredArgsConstructor
 
28
  public class GameCharacterServiceImpl implements GameCharacterService {
29
 
30
  private final GameCharacterRepository gameCharacterRepository;
@@ -40,17 +41,18 @@ public class GameCharacterServiceImpl implements GameCharacterService {
40
  @Override
41
  public GameCharacterResponse createGameCharacter(
42
  CreateGameCharacterRequest request) {
43
-
44
  if (request == null) {
45
  throw new IllegalArgumentException(
46
  "Không thể tạo nhân vật với dữ liệu null");
47
  }
48
 
49
  GameCharacter character = request.toEntity();
50
- character.setCreatedBy(getCurrentUser());
51
 
52
  GameCharacter savedCharacter = gameCharacterRepository.save(character);
53
  auditLogService.log(
 
54
  LogEntityType.CHARACTER,
55
  savedCharacter.getId(),
56
  LogAction.CREATE,
@@ -62,6 +64,7 @@ public class GameCharacterServiceImpl implements GameCharacterService {
62
  @Transactional
63
  @Override
64
  public GameCharacterResponse updateGameCharacter(UpdateGameCharacterRequest request) {
 
65
  if (request == null) {
66
  throw new IllegalArgumentException(
67
  "Không thể thay đổi nhân vật với dữ liệu null");
@@ -72,11 +75,12 @@ public class GameCharacterServiceImpl implements GameCharacterService {
72
  character.setDescription(request.getDescription());
73
  character.setQuote(request.getQuote());
74
  character.setPriority(request.getPriority());
75
- character.setUpdatedBy(getCurrentUser());
76
  if (request.getImage() != null) {
77
  character.setImage(request.getImage());
78
  }
79
  auditLogService.log(
 
80
  LogEntityType.CHARACTER,
81
  character.getId(),
82
  LogAction.UPDATE,
@@ -88,13 +92,15 @@ public class GameCharacterServiceImpl implements GameCharacterService {
88
  @Transactional
89
  @Override
90
  public void deleteCharacter(Long id) {
 
91
  if (id == null) {
92
  throw new IllegalArgumentException("Không thể xóa nhân vật với ID null");
93
  }
94
  GameCharacter found = findGameCharacter(id);
95
  found.setDeleted(true);
96
- found.setUpdatedBy(getCurrentUser());
97
  auditLogService.log(
 
98
  LogEntityType.CHARACTER,
99
  found.getId(),
100
  LogAction.DELETE,
@@ -104,13 +110,15 @@ public class GameCharacterServiceImpl implements GameCharacterService {
104
  @Transactional
105
  @Override
106
  public void restoreCharacter(Long id) {
 
107
  if (id == null) {
108
  throw new IllegalArgumentException("Không thể khôi phục nhân vật với ID null");
109
  }
110
  GameCharacter found = findGameCharacter(id);
111
  found.setDeleted(false);
112
- found.setUpdatedBy(getCurrentUser());
113
- auditLogService.log(
 
114
  LogEntityType.CHARACTER,
115
  found.getId(),
116
  LogAction.RESTORE,
@@ -154,15 +162,10 @@ public class GameCharacterServiceImpl implements GameCharacterService {
154
  }
155
 
156
  private User getCurrentUser() {
157
-
158
- String currentUsername = SecurityUtil.getCurrentUserName();
159
-
160
- if (currentUsername == null) {
161
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
162
  }
163
-
164
- return userRepository
165
- .findUserByUsername(currentUsername)
166
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng"));
167
  }
168
  }
 
25
 
26
  @Service
27
  @RequiredArgsConstructor
28
+ @Transactional(readOnly = true)
29
  public class GameCharacterServiceImpl implements GameCharacterService {
30
 
31
  private final GameCharacterRepository gameCharacterRepository;
 
41
  @Override
42
  public GameCharacterResponse createGameCharacter(
43
  CreateGameCharacterRequest request) {
44
+ User currentUser = getCurrentUser();
45
  if (request == null) {
46
  throw new IllegalArgumentException(
47
  "Không thể tạo nhân vật với dữ liệu null");
48
  }
49
 
50
  GameCharacter character = request.toEntity();
51
+ character.setCreatedBy(currentUser);
52
 
53
  GameCharacter savedCharacter = gameCharacterRepository.save(character);
54
  auditLogService.log(
55
+ currentUser,
56
  LogEntityType.CHARACTER,
57
  savedCharacter.getId(),
58
  LogAction.CREATE,
 
64
  @Transactional
65
  @Override
66
  public GameCharacterResponse updateGameCharacter(UpdateGameCharacterRequest request) {
67
+ User currentUser = getCurrentUser();
68
  if (request == null) {
69
  throw new IllegalArgumentException(
70
  "Không thể thay đổi nhân vật với dữ liệu null");
 
75
  character.setDescription(request.getDescription());
76
  character.setQuote(request.getQuote());
77
  character.setPriority(request.getPriority());
78
+ character.setUpdatedBy(currentUser);
79
  if (request.getImage() != null) {
80
  character.setImage(request.getImage());
81
  }
82
  auditLogService.log(
83
+ currentUser,
84
  LogEntityType.CHARACTER,
85
  character.getId(),
86
  LogAction.UPDATE,
 
92
  @Transactional
93
  @Override
94
  public void deleteCharacter(Long id) {
95
+ User currentUser = getCurrentUser();
96
  if (id == null) {
97
  throw new IllegalArgumentException("Không thể xóa nhân vật với ID null");
98
  }
99
  GameCharacter found = findGameCharacter(id);
100
  found.setDeleted(true);
101
+ found.setUpdatedBy(currentUser);
102
  auditLogService.log(
103
+ currentUser,
104
  LogEntityType.CHARACTER,
105
  found.getId(),
106
  LogAction.DELETE,
 
110
  @Transactional
111
  @Override
112
  public void restoreCharacter(Long id) {
113
+ User currentUser = getCurrentUser();
114
  if (id == null) {
115
  throw new IllegalArgumentException("Không thể khôi phục nhân vật với ID null");
116
  }
117
  GameCharacter found = findGameCharacter(id);
118
  found.setDeleted(false);
119
+ found.setUpdatedBy(currentUser);
120
+ auditLogService.log(
121
+ currentUser,
122
  LogEntityType.CHARACTER,
123
  found.getId(),
124
  LogAction.RESTORE,
 
162
  }
163
 
164
  private User getCurrentUser() {
165
+ Long currentUserId = SecurityUtil.getCurrentUserId();
166
+ if (currentUserId == null) {
 
 
167
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
168
  }
169
+ return userRepository.getReferenceById(currentUserId);
 
 
 
170
  }
171
  }
src/main/java/com/darkfantasy/service/impl/StoryServiceImpl.java CHANGED
@@ -24,6 +24,7 @@ import lombok.RequiredArgsConstructor;
24
 
25
  @Service
26
  @RequiredArgsConstructor
 
27
  public class StoryServiceImpl implements StoryService {
28
  private final StoryRepository storyRepository;
29
  private final UserRepository userRepository;
@@ -44,6 +45,7 @@ public class StoryServiceImpl implements StoryService {
44
  @Transactional
45
  @Override
46
  public StoryResponse createStory(CreateStoryRequest request) {
 
47
  if (request == null) {
48
  throw new IllegalArgumentException(
49
  "Không thể tạo câu chuyện với dữ liệu null");
@@ -51,10 +53,11 @@ public class StoryServiceImpl implements StoryService {
51
 
52
  Story story = request.toEntity();
53
 
54
- story.setCreatedBy(getCurrentUser());
55
 
56
  Story savedStory = storyRepository.save(story);
57
  auditLogService.log(
 
58
  LogEntityType.STORY,
59
  savedStory.getId(),
60
  LogAction.CREATE,
@@ -65,6 +68,7 @@ public class StoryServiceImpl implements StoryService {
65
  @Transactional
66
  @Override
67
  public StoryResponse updateStory(UpdateStoryRequest request) {
 
68
  if (request == null) {
69
  throw new IllegalArgumentException(
70
  "Không thể thay đổi câu chuyện với dữ liệu null");
@@ -79,8 +83,9 @@ public class StoryServiceImpl implements StoryService {
79
  story.setQuoteContent(request.getQuoteContent());
80
  story.setQuoteAuthor(request.getQuoteAuthor());
81
  story.setPriority(request.getPriority());
82
- story.setUpdatedBy(getCurrentUser());
83
  auditLogService.log(
 
84
  LogEntityType.STORY,
85
  story.getId(),
86
  LogAction.UPDATE,
@@ -91,13 +96,15 @@ public class StoryServiceImpl implements StoryService {
91
  @Transactional
92
  @Override
93
  public void deleteStory(Long id) {
 
94
  if (id == null) {
95
  throw new IllegalArgumentException("Không thể xóa câu chuyện với ID null");
96
  }
97
  Story found = findStory(id);
98
  found.setDeleted(true);
99
- found.setUpdatedBy(getCurrentUser());
100
  auditLogService.log(
 
101
  LogEntityType.STORY,
102
  found.getId(),
103
  LogAction.DELETE,
@@ -107,13 +114,15 @@ public class StoryServiceImpl implements StoryService {
107
  @Transactional
108
  @Override
109
  public void restoreStory(Long id) {
 
110
  if (id == null) {
111
  throw new IllegalArgumentException("Không thể khôi phục câu chuyện với ID null");
112
  }
113
  Story found = findStory(id);
114
  found.setDeleted(false);
115
- found.setUpdatedBy(getCurrentUser());
116
  auditLogService.log(
 
117
  LogEntityType.STORY,
118
  found.getId(),
119
  LogAction.RESTORE,
@@ -123,7 +132,7 @@ public class StoryServiceImpl implements StoryService {
123
  @Override
124
  public StoryResponse getStoryDeletedFalseHighestPriority() {
125
  Story story = storyRepository.findTopByDeletedFalseOrderByPriorityDesc();
126
- if(story == null){
127
  return null;
128
  }
129
  return StoryResponse.fromEntity(story);
@@ -149,15 +158,10 @@ public class StoryServiceImpl implements StoryService {
149
  }
150
 
151
  private User getCurrentUser() {
152
-
153
- String currentUsername = SecurityUtil.getCurrentUserName();
154
-
155
- if (currentUsername == null) {
156
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
157
  }
158
-
159
- return userRepository
160
- .findUserByUsername(currentUsername)
161
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng"));
162
  }
163
  }
 
24
 
25
  @Service
26
  @RequiredArgsConstructor
27
+ @Transactional(readOnly = true)
28
  public class StoryServiceImpl implements StoryService {
29
  private final StoryRepository storyRepository;
30
  private final UserRepository userRepository;
 
45
  @Transactional
46
  @Override
47
  public StoryResponse createStory(CreateStoryRequest request) {
48
+ User currentUser = getCurrentUser();
49
  if (request == null) {
50
  throw new IllegalArgumentException(
51
  "Không thể tạo câu chuyện với dữ liệu null");
 
53
 
54
  Story story = request.toEntity();
55
 
56
+ story.setCreatedBy(currentUser);
57
 
58
  Story savedStory = storyRepository.save(story);
59
  auditLogService.log(
60
+ currentUser,
61
  LogEntityType.STORY,
62
  savedStory.getId(),
63
  LogAction.CREATE,
 
68
  @Transactional
69
  @Override
70
  public StoryResponse updateStory(UpdateStoryRequest request) {
71
+ User currentUser = getCurrentUser();
72
  if (request == null) {
73
  throw new IllegalArgumentException(
74
  "Không thể thay đổi câu chuyện với dữ liệu null");
 
83
  story.setQuoteContent(request.getQuoteContent());
84
  story.setQuoteAuthor(request.getQuoteAuthor());
85
  story.setPriority(request.getPriority());
86
+ story.setUpdatedBy(currentUser);
87
  auditLogService.log(
88
+ currentUser,
89
  LogEntityType.STORY,
90
  story.getId(),
91
  LogAction.UPDATE,
 
96
  @Transactional
97
  @Override
98
  public void deleteStory(Long id) {
99
+ User currentUser = getCurrentUser();
100
  if (id == null) {
101
  throw new IllegalArgumentException("Không thể xóa câu chuyện với ID null");
102
  }
103
  Story found = findStory(id);
104
  found.setDeleted(true);
105
+ found.setUpdatedBy(currentUser);
106
  auditLogService.log(
107
+ currentUser,
108
  LogEntityType.STORY,
109
  found.getId(),
110
  LogAction.DELETE,
 
114
  @Transactional
115
  @Override
116
  public void restoreStory(Long id) {
117
+ User currentUser = getCurrentUser();
118
  if (id == null) {
119
  throw new IllegalArgumentException("Không thể khôi phục câu chuyện với ID null");
120
  }
121
  Story found = findStory(id);
122
  found.setDeleted(false);
123
+ found.setUpdatedBy(currentUser);
124
  auditLogService.log(
125
+ currentUser,
126
  LogEntityType.STORY,
127
  found.getId(),
128
  LogAction.RESTORE,
 
132
  @Override
133
  public StoryResponse getStoryDeletedFalseHighestPriority() {
134
  Story story = storyRepository.findTopByDeletedFalseOrderByPriorityDesc();
135
+ if (story == null) {
136
  return null;
137
  }
138
  return StoryResponse.fromEntity(story);
 
158
  }
159
 
160
  private User getCurrentUser() {
161
+ Long currentUserId = SecurityUtil.getCurrentUserId();
162
+ if (currentUserId == null) {
 
 
163
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
164
  }
165
+ return userRepository.getReferenceById(currentUserId);
 
 
 
166
  }
167
  }
src/main/java/com/darkfantasy/service/impl/UserServiceImpl.java CHANGED
@@ -3,11 +3,9 @@ package com.darkfantasy.service.impl;
3
  import java.security.SecureRandom;
4
  import java.time.Instant;
5
  import java.util.Optional;
6
- import java.util.Random;
7
 
8
  import org.springframework.data.domain.Page;
9
  import org.springframework.data.domain.Pageable;
10
- import org.springframework.mail.SimpleMailMessage;
11
  import org.springframework.mail.javamail.JavaMailSender;
12
  import org.springframework.security.authentication.AuthenticationManager;
13
  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -34,6 +32,7 @@ import com.darkfantasy.exception.custom.ResourceNotFoundException;
34
  import com.darkfantasy.repository.PasswordResetTokenRepository;
35
  import com.darkfantasy.repository.UserRepository;
36
  import com.darkfantasy.service.AuditLogService;
 
37
  import com.darkfantasy.service.UserService;
38
  import com.darkfantasy.util.SecurityUtil;
39
 
@@ -41,13 +40,14 @@ import lombok.RequiredArgsConstructor;
41
 
42
  @Service
43
  @RequiredArgsConstructor
 
44
  public class UserServiceImpl implements UserService {
45
  private final UserRepository userRepository;
46
  private final PasswordEncoder passwordEncoder;
47
  private final AuditLogService auditLogService;
48
  private final PasswordResetTokenRepository tokenRepository;
49
- private final JavaMailSender mailSender;
50
  private final AuthenticationManager authenticationManager;
 
51
 
52
  @Transactional
53
  @Override
@@ -116,24 +116,13 @@ public class UserServiceImpl implements UserService {
116
  currentUser.setMustChangePassword(false);
117
 
118
  auditLogService.log(
 
119
  LogEntityType.USER,
120
  currentUser.getId(),
121
  LogAction.UPDATE,
122
  "Đổi mật khẩu: " + currentUser.getUsername());
123
  }
124
 
125
- private Optional<User> findUser(String keyword) {
126
- return userRepository.findUserByUsernameOrEmail(keyword, keyword);
127
- }
128
-
129
- private boolean existsByKeyword(String keyword) {
130
- return userRepository.existsByUsernameOrEmail(keyword, keyword);
131
- }
132
-
133
- private User findUser(Long id) {
134
- return id == null ? null : userRepository.getReferenceById(id);
135
- }
136
-
137
  @Override
138
  public UserResponse findAccountById(Long id) {
139
  if (id == null)
@@ -143,17 +132,6 @@ public class UserServiceImpl implements UserService {
143
  return UserResponse.fromEntity(user);
144
  }
145
 
146
- private User getCurrentUser() {
147
- String currentUsername = SecurityUtil.getCurrentUserName();
148
-
149
- if (currentUsername == null) {
150
- throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
151
- }
152
-
153
- return userRepository.findUserByUsername(currentUsername)
154
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng"));
155
- }
156
-
157
  @Override
158
  public Page<UserResponse> getAccounts(Pageable pageable) {
159
  return userRepository.findAllByOrderByIdAsc(pageable)
@@ -163,11 +141,12 @@ public class UserServiceImpl implements UserService {
163
  @Transactional
164
  @Override
165
  public void lockUser(Long id) {
166
- User user = findUser(id);
167
  if (user.getRole() == Role.ADMIN)
168
  throw new IllegalArgumentException("Không có thẩm quyền");
169
  user.setActive(false);
170
  auditLogService.log(
 
171
  LogEntityType.USER,
172
  user.getId(),
173
  LogAction.LOCK,
@@ -177,11 +156,12 @@ public class UserServiceImpl implements UserService {
177
  @Transactional
178
  @Override
179
  public void unlockUser(Long id) {
180
- User user = findUser(id);
181
  if (user.getRole() == Role.ADMIN)
182
  throw new IllegalArgumentException("Không có thẩm quyền");
183
  user.setActive(true);
184
  auditLogService.log(
 
185
  LogEntityType.USER,
186
  user.getId(),
187
  LogAction.UNLOCK,
@@ -224,14 +204,9 @@ public class UserServiceImpl implements UserService {
224
  .used(false)
225
  .build();
226
  tokenRepository.save(token);
227
- SimpleMailMessage message = new SimpleMailMessage();
228
- message.setTo(user.getEmail());
229
- message.setSubject("Mã OTP đ���t lại mật khẩu");
230
- message.setText("Mã OTP của bạn là: " + otp + "\n\nMã có hiệu lực trong 5 phút.");
231
- mailSender.send(message);
232
  }
233
 
234
- @Transactional(readOnly = true)
235
  @Override
236
  public void verifyOtp(VerifyOtpRequest request) {
237
  PasswordResetToken token = tokenRepository
@@ -261,4 +236,40 @@ public class UserServiceImpl implements UserService {
261
  user.setPassword(passwordEncoder.encode(request.getNewPassword()));
262
  token.setUsed(true);
263
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
 
3
  import java.security.SecureRandom;
4
  import java.time.Instant;
5
  import java.util.Optional;
 
6
 
7
  import org.springframework.data.domain.Page;
8
  import org.springframework.data.domain.Pageable;
 
9
  import org.springframework.mail.javamail.JavaMailSender;
10
  import org.springframework.security.authentication.AuthenticationManager;
11
  import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 
32
  import com.darkfantasy.repository.PasswordResetTokenRepository;
33
  import com.darkfantasy.repository.UserRepository;
34
  import com.darkfantasy.service.AuditLogService;
35
+ import com.darkfantasy.service.EmailService;
36
  import com.darkfantasy.service.UserService;
37
  import com.darkfantasy.util.SecurityUtil;
38
 
 
40
 
41
  @Service
42
  @RequiredArgsConstructor
43
+ @Transactional(readOnly = true)
44
  public class UserServiceImpl implements UserService {
45
  private final UserRepository userRepository;
46
  private final PasswordEncoder passwordEncoder;
47
  private final AuditLogService auditLogService;
48
  private final PasswordResetTokenRepository tokenRepository;
 
49
  private final AuthenticationManager authenticationManager;
50
+ private final EmailService emailService;
51
 
52
  @Transactional
53
  @Override
 
116
  currentUser.setMustChangePassword(false);
117
 
118
  auditLogService.log(
119
+ currentUser,
120
  LogEntityType.USER,
121
  currentUser.getId(),
122
  LogAction.UPDATE,
123
  "Đổi mật khẩu: " + currentUser.getUsername());
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  @Override
127
  public UserResponse findAccountById(Long id) {
128
  if (id == null)
 
132
  return UserResponse.fromEntity(user);
133
  }
134
 
 
 
 
 
 
 
 
 
 
 
 
135
  @Override
136
  public Page<UserResponse> getAccounts(Pageable pageable) {
137
  return userRepository.findAllByOrderByIdAsc(pageable)
 
141
  @Transactional
142
  @Override
143
  public void lockUser(Long id) {
144
+ User user = findUserById(id);
145
  if (user.getRole() == Role.ADMIN)
146
  throw new IllegalArgumentException("Không có thẩm quyền");
147
  user.setActive(false);
148
  auditLogService.log(
149
+ getCurrentUser(),
150
  LogEntityType.USER,
151
  user.getId(),
152
  LogAction.LOCK,
 
156
  @Transactional
157
  @Override
158
  public void unlockUser(Long id) {
159
+ User user = findUserById(id);
160
  if (user.getRole() == Role.ADMIN)
161
  throw new IllegalArgumentException("Không có thẩm quyền");
162
  user.setActive(true);
163
  auditLogService.log(
164
+ getCurrentUser(),
165
  LogEntityType.USER,
166
  user.getId(),
167
  LogAction.UNLOCK,
 
204
  .used(false)
205
  .build();
206
  tokenRepository.save(token);
207
+ emailService.sendPasswordResetEmail(user.getEmail(), otp);
 
 
 
 
208
  }
209
 
 
210
  @Override
211
  public void verifyOtp(VerifyOtpRequest request) {
212
  PasswordResetToken token = tokenRepository
 
236
  user.setPassword(passwordEncoder.encode(request.getNewPassword()));
237
  token.setUsed(true);
238
  }
239
+
240
+ @Override
241
+ public void logLogout() {
242
+ User currentUser = getCurrentUser();
243
+ auditLogService.log(
244
+ currentUser,
245
+ LogEntityType.USER,
246
+ currentUser.getId(),
247
+ LogAction.LOGOUT,
248
+ "Đăng xuất: " + currentUser.getUsername());
249
+ }
250
+
251
+ private Optional<User> findUser(String keyword) {
252
+ return userRepository.findUserByUsernameOrEmail(keyword, keyword);
253
+ }
254
+
255
+ private boolean existsByKeyword(String keyword) {
256
+ return userRepository.existsByUsernameOrEmail(keyword, keyword);
257
+ }
258
+
259
+ private User findUserById(Long id) {
260
+ if (id == null) {
261
+ throw new IllegalArgumentException("Id không thể là null");
262
+ }
263
+ return userRepository.findById(id)
264
+ .orElseThrow(() -> new ResourceNotFoundException("Không tìm thấy người dùng"));
265
+ }
266
+
267
+ private User getCurrentUser() {
268
+ Long currentUserId = SecurityUtil.getCurrentUserId();
269
+ if (currentUserId == null) {
270
+ throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
271
+ }
272
+ return userRepository.getReferenceById(currentUserId);
273
+ }
274
+
275
  }
src/main/java/com/darkfantasy/service/impl/WorldServiceImpl.java CHANGED
@@ -2,6 +2,8 @@ package com.darkfantasy.service.impl;
2
 
3
  import java.util.List;
4
 
 
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
  import org.springframework.stereotype.Service;
@@ -24,6 +26,7 @@ import lombok.RequiredArgsConstructor;
24
 
25
  @Service
26
  @RequiredArgsConstructor
 
27
  public class WorldServiceImpl implements WorldService {
28
 
29
  private final WorldRepository worldRepository;
@@ -42,28 +45,33 @@ public class WorldServiceImpl implements WorldService {
42
  return WorldResponse.fromEntity(findWorld(id));
43
  }
44
 
 
45
  @Transactional
46
  @Override
47
  public WorldResponse createWorld(CreateWorldRequest request) {
 
48
  if (request == null) {
49
  throw new IllegalArgumentException(
50
  "Không thể tạo thế giới với dữ liệu null");
51
  }
52
 
53
  World world = request.toEntity();
54
- world.setCreatedBy(getCurrentUser());
55
  World savedWorld = worldRepository.save(world);
56
  auditLogService.log(
 
57
  LogEntityType.WORLD,
58
  savedWorld.getId(),
59
  LogAction.CREATE,
60
  "Thêm thế giới: " + savedWorld.getTitle());
61
- return WorldResponse.fromEntity(world);
62
  }
63
 
 
64
  @Transactional
65
  @Override
66
  public WorldResponse updateWorld(UpdateWorldRequest request) {
 
67
  if (request == null) {
68
  throw new IllegalArgumentException(
69
  "Không thể thay đổi thế giới với dữ liệu null");
@@ -76,8 +84,9 @@ public class WorldServiceImpl implements WorldService {
76
  if (request.getImage() != null) {
77
  world.setImage(request.getImage());
78
  }
79
- world.setUpdatedBy(getCurrentUser());
80
  auditLogService.log(
 
81
  LogEntityType.WORLD,
82
  world.getId(),
83
  LogAction.UPDATE,
@@ -85,32 +94,38 @@ public class WorldServiceImpl implements WorldService {
85
  return WorldResponse.fromEntity(world);
86
  }
87
 
 
88
  @Transactional
89
  @Override
90
  public void deleteWorld(Long id) {
 
91
  if (id == null) {
92
  throw new IllegalArgumentException("Không thể xóa thế giới với ID null");
93
  }
94
  World found = findWorld(id);
95
  found.setDeleted(true);
96
- found.setCreatedBy(getCurrentUser());
97
  auditLogService.log(
 
98
  LogEntityType.WORLD,
99
  found.getId(),
100
  LogAction.DELETE,
101
  "Xóa thế giới: " + found.getTitle());
102
  }
103
 
 
104
  @Transactional
105
  @Override
106
  public void restoreWorld(Long id) {
 
107
  if (id == null) {
108
  throw new IllegalArgumentException("Không thể khôi phục thế giới với ID null");
109
  }
110
  World found = findWorld(id);
111
  found.setDeleted(false);
112
- found.setUpdatedBy(getCurrentUser());
113
  auditLogService.log(
 
114
  LogEntityType.WORLD,
115
  found.getId(),
116
  LogAction.RESTORE,
@@ -129,6 +144,7 @@ public class WorldServiceImpl implements WorldService {
129
  return WorldResponse.fromEntity(world);
130
  }
131
 
 
132
  @Override
133
  public List<WorldResponse> getWorldsDeletedFalse() {
134
  return worldRepository
@@ -138,27 +154,22 @@ public class WorldServiceImpl implements WorldService {
138
  .toList();
139
  }
140
 
141
- private World findWorld(Long id) {
142
- return worldRepository.findById(id)
143
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy thế giới với ID: " + id));
144
- }
145
-
146
  @Override
147
  public long count() {
148
  return worldRepository.count();
149
  }
150
 
151
  private User getCurrentUser() {
152
-
153
- String currentUsername = SecurityUtil.getCurrentUserName();
154
-
155
- if (currentUsername == null) {
156
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
157
  }
 
 
158
 
159
- return userRepository
160
- .findUserByUsername(currentUsername)
161
- .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng"));
162
  }
163
 
164
  }
 
2
 
3
  import java.util.List;
4
 
5
+ import org.springframework.cache.annotation.CacheEvict;
6
+ import org.springframework.cache.annotation.Cacheable;
7
  import org.springframework.data.domain.Page;
8
  import org.springframework.data.domain.Pageable;
9
  import org.springframework.stereotype.Service;
 
26
 
27
  @Service
28
  @RequiredArgsConstructor
29
+ @Transactional(readOnly = true)
30
  public class WorldServiceImpl implements WorldService {
31
 
32
  private final WorldRepository worldRepository;
 
45
  return WorldResponse.fromEntity(findWorld(id));
46
  }
47
 
48
+ @CacheEvict(value = "publicWorlds", allEntries = true)
49
  @Transactional
50
  @Override
51
  public WorldResponse createWorld(CreateWorldRequest request) {
52
+ User currentUser = getCurrentUser();
53
  if (request == null) {
54
  throw new IllegalArgumentException(
55
  "Không thể tạo thế giới với dữ liệu null");
56
  }
57
 
58
  World world = request.toEntity();
59
+ world.setCreatedBy(currentUser);
60
  World savedWorld = worldRepository.save(world);
61
  auditLogService.log(
62
+ currentUser,
63
  LogEntityType.WORLD,
64
  savedWorld.getId(),
65
  LogAction.CREATE,
66
  "Thêm thế giới: " + savedWorld.getTitle());
67
+ return WorldResponse.fromEntity(savedWorld);
68
  }
69
 
70
+ @CacheEvict(value = "publicWorlds", allEntries = true)
71
  @Transactional
72
  @Override
73
  public WorldResponse updateWorld(UpdateWorldRequest request) {
74
+ User currentUser = getCurrentUser();
75
  if (request == null) {
76
  throw new IllegalArgumentException(
77
  "Không thể thay đổi thế giới với dữ liệu null");
 
84
  if (request.getImage() != null) {
85
  world.setImage(request.getImage());
86
  }
87
+ world.setUpdatedBy(currentUser);
88
  auditLogService.log(
89
+ currentUser,
90
  LogEntityType.WORLD,
91
  world.getId(),
92
  LogAction.UPDATE,
 
94
  return WorldResponse.fromEntity(world);
95
  }
96
 
97
+ @CacheEvict(value = "publicWorlds", allEntries = true)
98
  @Transactional
99
  @Override
100
  public void deleteWorld(Long id) {
101
+ User currentUser = getCurrentUser();
102
  if (id == null) {
103
  throw new IllegalArgumentException("Không thể xóa thế giới với ID null");
104
  }
105
  World found = findWorld(id);
106
  found.setDeleted(true);
107
+ found.setUpdatedBy(currentUser);
108
  auditLogService.log(
109
+ currentUser,
110
  LogEntityType.WORLD,
111
  found.getId(),
112
  LogAction.DELETE,
113
  "Xóa thế giới: " + found.getTitle());
114
  }
115
 
116
+ @CacheEvict(value = "publicWorlds", allEntries = true)
117
  @Transactional
118
  @Override
119
  public void restoreWorld(Long id) {
120
+ User currentUser = getCurrentUser();
121
  if (id == null) {
122
  throw new IllegalArgumentException("Không thể khôi phục thế giới với ID null");
123
  }
124
  World found = findWorld(id);
125
  found.setDeleted(false);
126
+ found.setUpdatedBy(currentUser);
127
  auditLogService.log(
128
+ currentUser,
129
  LogEntityType.WORLD,
130
  found.getId(),
131
  LogAction.RESTORE,
 
144
  return WorldResponse.fromEntity(world);
145
  }
146
 
147
+ @Cacheable(value = "publicWorlds")
148
  @Override
149
  public List<WorldResponse> getWorldsDeletedFalse() {
150
  return worldRepository
 
154
  .toList();
155
  }
156
 
 
 
 
 
 
157
  @Override
158
  public long count() {
159
  return worldRepository.count();
160
  }
161
 
162
  private User getCurrentUser() {
163
+ Long currentUserId = SecurityUtil.getCurrentUserId();
164
+ if (currentUserId == null) {
 
 
165
  throw new IllegalStateException("Không tìm thấy người dùng hiện tại");
166
  }
167
+ return userRepository.getReferenceById(currentUserId);
168
+ }
169
 
170
+ private World findWorld(Long id) {
171
+ return worldRepository.findById(id)
172
+ .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy thế giới với ID: " + id));
173
  }
174
 
175
  }
src/main/java/com/darkfantasy/util/SecurityUtil.java CHANGED
@@ -2,46 +2,38 @@ package com.darkfantasy.util;
2
 
3
  import org.springframework.security.authentication.AnonymousAuthenticationToken;
4
  import org.springframework.security.core.Authentication;
5
- import org.springframework.security.core.GrantedAuthority;
6
  import org.springframework.security.core.context.SecurityContextHolder;
7
 
8
  import com.darkfantasy.entity.enums.Role;
 
9
 
10
  import lombok.experimental.UtilityClass;
11
 
12
  @UtilityClass
13
  public class SecurityUtil {
14
- public String getCurrentUserName() {
15
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
16
- if (authentication == null
17
- || !authentication.isAuthenticated()
18
- || authentication instanceof AnonymousAuthenticationToken) {
19
- return null;
20
  }
21
- return authentication.getName();
22
- }
23
 
24
- public Role getCurrentUserRole() {
 
25
 
26
- Authentication authentication = SecurityContextHolder
27
- .getContext()
28
- .getAuthentication();
 
 
29
 
30
- if (authentication == null
31
- || !authentication.isAuthenticated()
32
- || authentication instanceof AnonymousAuthenticationToken) {
33
 
34
- return null;
35
- }
 
36
 
37
- return authentication
38
- .getAuthorities()
39
- .stream()
40
- .map(GrantedAuthority::getAuthority)
41
- .filter(authority -> authority.startsWith("ROLE_"))
42
- .findFirst()
43
- .map(authority -> Role.valueOf(
44
- authority.replace("ROLE_", "")))
45
- .orElse(null);
46
  }
47
  }
 
2
 
3
  import org.springframework.security.authentication.AnonymousAuthenticationToken;
4
  import org.springframework.security.core.Authentication;
 
5
  import org.springframework.security.core.context.SecurityContextHolder;
6
 
7
  import com.darkfantasy.entity.enums.Role;
8
+ import com.darkfantasy.security.CustomUserDetails;
9
 
10
  import lombok.experimental.UtilityClass;
11
 
12
  @UtilityClass
13
  public class SecurityUtil {
14
+ public CustomUserDetails getCurrentUser() {
15
+ if (!isAuthenticated()) {
16
+ throw new IllegalStateException("Người dùng chưa đăng nhập");
 
 
 
17
  }
 
 
18
 
19
+ return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
20
+ }
21
 
22
+ public boolean isAuthenticated() {
23
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
24
+ return authentication != null && authentication.isAuthenticated()
25
+ && !(authentication instanceof AnonymousAuthenticationToken);
26
+ }
27
 
28
+ public Role getCurrentUserRole() {
29
+ return Role.valueOf(getCurrentUser().getRole());
30
+ }
31
 
32
+ public String getCurrentUserName() {
33
+ return getCurrentUser().getUsername();
34
+ }
35
 
36
+ public Long getCurrentUserId() {
37
+ return getCurrentUser().getId();
 
 
 
 
 
 
 
38
  }
39
  }
src/main/resources/application.properties CHANGED
@@ -26,7 +26,7 @@ server.servlet.jsp.init-parameters.development=true
26
  spring.datasource.url=${SPRING_DATASOURCE_URL}
27
  # spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
28
  # spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
29
-
30
  # Hibernate Dialect
31
  spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
32
 
@@ -38,8 +38,16 @@ spring.mail.password=${SPRING_MAIL_PASSWORD}
38
 
39
  # JPA configuration
40
  spring.jpa.hibernate.ddl-auto=update
41
- spring.jpa.show-sql=true
42
 
 
 
 
 
 
 
 
 
 
43
 
44
 
45
  spring.mail.properties.mail.smtp.auth=true
@@ -56,3 +64,27 @@ spring.cache.type=none
56
  # spring.cache.type=caffeine
57
  # spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=5m
58
  # spring.cache.cache-names=articles,characters,worlds,stories,faqs,contributors
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  spring.datasource.url=${SPRING_DATASOURCE_URL}
27
  # spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
28
  # spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
29
+ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
30
  # Hibernate Dialect
31
  spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
32
 
 
38
 
39
  # JPA configuration
40
  spring.jpa.hibernate.ddl-auto=update
 
41
 
42
+ # Bật tính năng in câu lệnh SQL ra console
43
+ #Nên bật khi code, khi deploy thì tắt
44
+ spring.jpa.show-sql=false
45
+
46
+ # Format SQL cho dễ đọc (xuống dòng, thụt lề), nếu không nó sẽ in thành 1 hàng ngang rất rối
47
+ spring.jpa.properties.hibernate.format_sql=true
48
+
49
+ # Làm nổi bật các từ khóa SQL (SELECT, FROM, JOIN) bằng màu sắc
50
+ spring.jpa.properties.hibernate.highlight_sql=true
51
 
52
 
53
  spring.mail.properties.mail.smtp.auth=true
 
64
  # spring.cache.type=caffeine
65
  # spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=5m
66
  # spring.cache.cache-names=articles,characters,worlds,stories,faqs,contributors
67
+
68
+
69
+ # 1. TỐI ƯU CONNECTION POOL (HikariCP)
70
+ # Số lượng kết nối tối đa giữ với Database (Tránh mở quá nhiều gây sập DB)
71
+ spring.datasource.hikari.maximum-pool-size=20
72
+ spring.datasource.hikari.minimum-idle=5
73
+ # Thời gian chờ tối đa nếu hết kết nối (tính bằng ms)
74
+ spring.datasource.hikari.connection-timeout=30000
75
+
76
+ # 2. BẬT BATCH PROCESSING (Đóng gói câu SQL)
77
+ # Thay vì bắn 100 câu INSERT rời rạc, Hibernate sẽ gộp lại bắn 1 lần
78
+ spring.jpa.properties.hibernate.jdbc.batch_size=50
79
+ spring.jpa.properties.hibernate.order_inserts=true
80
+ spring.jpa.properties.hibernate.order_updates=true
81
+
82
+ # 3. TẮT OPEN-IN-VIEW (Quan trọng)
83
+ # Chặn việc Hibernate tự ý giữ kết nối Database trong lúc Controller đang trả về JSON (Anti-pattern)
84
+ spring.jpa.open-in-view=false
85
+
86
+
87
+
88
+
89
+ spring.config.import=optional:file:.env[.properties]
90
+