birc-generator 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.bircrc.example.json +14 -0
  2. package/AGENTS.md +20 -0
  3. package/PROJECT.md +83 -0
  4. package/README.md +300 -0
  5. package/bin/birc.js +645 -0
  6. package/bin/cli-util.js +428 -0
  7. package/bin/home-install.js +346 -0
  8. package/bin/update-check.js +153 -0
  9. package/implement.md +76 -0
  10. package/install.ps1 +156 -0
  11. package/install.sh +201 -0
  12. package/package.json +68 -0
  13. package/plopfile.js +1358 -0
  14. package/project-docs/AGENTS.md.hbs +12 -0
  15. package/project-docs/PROJECT.md.hbs +128 -0
  16. package/project-docs/implement.hbs +54 -0
  17. package/project-docs/test.hbs +31 -0
  18. package/scripts/install-bin.sh +72 -0
  19. package/scripts/update.sh +11 -0
  20. package/templates/aop/OperationLog.hbs +18 -0
  21. package/templates/aop/OperationLogAspect.hbs +95 -0
  22. package/templates/aop/build-gradle-dep.hbs +1 -0
  23. package/templates/base/Application.hbs +11 -0
  24. package/templates/base/ApplicationTests.java.hbs +11 -0
  25. package/templates/base/BaseService.hbs +15 -0
  26. package/templates/base/BaseServiceImpl.hbs +53 -0
  27. package/templates/base/EntityMapper.hbs +12 -0
  28. package/templates/base/README.md.hbs +56 -0
  29. package/templates/base/Result.hbs +48 -0
  30. package/templates/base/application-test.yml.hbs +27 -0
  31. package/templates/base/application.yml.hbs +18 -0
  32. package/templates/base/bircrc.hbs +14 -0
  33. package/templates/base/build.gradle.hbs +33 -0
  34. package/templates/base/gitignore.hbs +15 -0
  35. package/templates/client/ExternalApiClient.hbs +28 -0
  36. package/templates/client/ExternalApiClientConfig.hbs +26 -0
  37. package/templates/client/ExternalApiProperties.hbs +30 -0
  38. package/templates/client/application-yml-block.hbs +7 -0
  39. package/templates/controller.hbs +55 -0
  40. package/templates/dao.hbs +11 -0
  41. package/templates/docker/Dockerfile.hbs +29 -0
  42. package/templates/docker/docker-compose.prod.yml.hbs +65 -0
  43. package/templates/docker/docker-compose.yml.hbs +52 -0
  44. package/templates/docker/dockerignore.hbs +6 -0
  45. package/templates/docker/env.example.hbs +24 -0
  46. package/templates/dto-request.hbs +19 -0
  47. package/templates/dto-response.hbs +13 -0
  48. package/templates/email/EmailAsyncConfig.hbs +28 -0
  49. package/templates/email/EmailService.hbs +26 -0
  50. package/templates/email/EmailServiceImpl.hbs +49 -0
  51. package/templates/email/application-yml-block.hbs +19 -0
  52. package/templates/email/build-gradle-dep.hbs +2 -0
  53. package/templates/email/sample-template.hbs +16 -0
  54. package/templates/entity.hbs +46 -0
  55. package/templates/exception/ExceptionHandleController.hbs +16 -0
  56. package/templates/exception/NotFoundException.hbs +28 -0
  57. package/templates/exception/ProjectException.hbs +20 -0
  58. package/templates/exception.hbs +30 -0
  59. package/templates/file-upload/EmptyFileException.hbs +25 -0
  60. package/templates/file-upload/FileExtensionIllegalException.hbs +21 -0
  61. package/templates/file-upload/FileExtensionUtils.hbs +20 -0
  62. package/templates/file-upload/FileStorageProperties.hbs +37 -0
  63. package/templates/file-upload/FileStorageService.hbs +15 -0
  64. package/templates/file-upload/FileStorageServiceImpl.hbs +78 -0
  65. package/templates/file-upload/FileTooLargeException.hbs +21 -0
  66. package/templates/file-upload/FileUploadController.hbs +57 -0
  67. package/templates/file-upload/FileUtils.hbs +39 -0
  68. package/templates/file-upload/InvalidStoredFileException.hbs +21 -0
  69. package/templates/file-upload/application-yml-block.hbs +12 -0
  70. package/templates/gitlab-ci/gitlab-ci.hbs +158 -0
  71. package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.jar +0 -0
  72. package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.properties +8 -0
  73. package/templates/gradle-wrapper/gradlew +248 -0
  74. package/templates/gradle-wrapper/gradlew.bat +93 -0
  75. package/templates/log4j2/build-gradle-dep.hbs +6 -0
  76. package/templates/log4j2/log4j2-spring.xml.hbs +34 -0
  77. package/templates/mapper-gradle-dep.hbs +3 -0
  78. package/templates/mapper.hbs +11 -0
  79. package/templates/migration/add-column.hbs +4 -0
  80. package/templates/migration/create-table.hbs +9 -0
  81. package/templates/migration/custom.hbs +2 -0
  82. package/templates/migration/remove-column.hbs +4 -0
  83. package/templates/migration/seed.hbs +7 -0
  84. package/templates/multi-module/build.gradle.hbs +86 -0
  85. package/templates/multi-module/config/ApplicationConfig.java.hbs +33 -0
  86. package/templates/multi-module/config/Config.java.hbs +19 -0
  87. package/templates/multi-module/config/SecurityConfig.java.hbs +48 -0
  88. package/templates/multi-module/config/build.gradle.hbs +10 -0
  89. package/templates/multi-module/database-config/BaseDAO.java.hbs +31 -0
  90. package/templates/multi-module/database-config/BaseViewDAO.java.hbs +48 -0
  91. package/templates/multi-module/database-config/Config.java.hbs +14 -0
  92. package/templates/multi-module/database-config/JpaConfig.java.hbs +9 -0
  93. package/templates/multi-module/database-config/build.gradle.hbs +17 -0
  94. package/templates/multi-module/settings.gradle.hbs +9 -0
  95. package/templates/oauth/OAuth2LoginSuccessHandler.hbs +31 -0
  96. package/templates/oauth/application-yml-block.hbs +11 -0
  97. package/templates/oauth/build-gradle-dep.hbs +1 -0
  98. package/templates/openapi/OpenApiConfig.hbs +23 -0
  99. package/templates/openapi/build-gradle-dep.hbs +1 -0
  100. package/templates/pagination/GenericSpecification.hbs +76 -0
  101. package/templates/pagination/PageInfo.hbs +20 -0
  102. package/templates/pagination/PageRequest.hbs +27 -0
  103. package/templates/pagination/PageResponse.hbs +24 -0
  104. package/templates/pagination/Pager.hbs +37 -0
  105. package/templates/pagination/SearchCriteria.hbs +26 -0
  106. package/templates/pagination/SpecificationSupport.hbs +34 -0
  107. package/templates/permission/PermissionAspect.hbs +41 -0
  108. package/templates/permission/RequirePermission.hbs +18 -0
  109. package/templates/permission/build-gradle-dep.hbs +2 -0
  110. package/templates/scheduling/SampleSchedule.hbs +22 -0
  111. package/templates/scheduling/SchedulingConfig.hbs +13 -0
  112. package/templates/scheduling/application-yml-block.hbs +5 -0
  113. package/templates/sentry/SentryConfig.hbs +74 -0
  114. package/templates/sentry/application-yml-block.hbs +15 -0
  115. package/templates/sentry/build-gradle-dep.hbs +1 -0
  116. package/templates/service.hbs +9 -0
  117. package/templates/serviceImpl.hbs +23 -0
  118. package/templates/spotless/build-gradle-allprojects.hbs +16 -0
  119. package/templates/spotless/build-gradle-plugin.hbs +1 -0
  120. package/templates/spotless/spotless_formatter.xml +83 -0
  121. package/templates/sso/SsoAutoConfiguration.hbs +17 -0
  122. package/templates/sso/SsoProperties.hbs +27 -0
  123. package/templates/sso/application-yml-block.hbs +7 -0
  124. package/templates/valid-group/ValidGroup.hbs +20 -0
  125. package/test.md +56 -0
  126. package/uninstall.ps1 +60 -0
  127. package/uninstall.sh +76 -0
  128. package/versions.js +10 -0
@@ -0,0 +1,48 @@
1
+ package {{daoPackage}};
2
+
3
+ import jakarta.annotation.Nonnull;
4
+ import java.io.Serializable;
5
+ import java.util.List;
6
+ import java.util.Optional;
7
+ import org.springframework.data.domain.Example;
8
+ import org.springframework.data.domain.Page;
9
+ import org.springframework.data.domain.Pageable;
10
+ import org.springframework.data.domain.Sort;
11
+ import org.springframework.data.repository.NoRepositoryBean;
12
+ import org.springframework.data.repository.Repository;
13
+ import org.springframework.data.repository.query.QueryByExampleExecutor;
14
+
15
+ @NoRepositoryBean
16
+ public interface BaseViewDAO<E, ID extends Serializable> extends Repository<E, ID>, QueryByExampleExecutor<E> {
17
+
18
+ @Nonnull
19
+ Optional<E> findById(@Nonnull ID id);
20
+
21
+ @Nonnull
22
+ List<E> findAll();
23
+
24
+ @Nonnull
25
+ List<E> findAllById(@Nonnull Iterable<ID> ids);
26
+
27
+ @Nonnull
28
+ List<E> findAll(@Nonnull Sort sort);
29
+
30
+ @Nonnull
31
+ Page<E> findAll(@Nonnull Pageable pageable);
32
+
33
+ @Override
34
+ @Nonnull
35
+ <S extends E> List<S> findAll(@Nonnull Example<S> example);
36
+
37
+ @Override
38
+ @Nonnull
39
+ <S extends E> List<S> findAll(@Nonnull Example<S> example, @Nonnull Sort sort);
40
+
41
+ @Override
42
+ @Nonnull
43
+ <S extends E> Page<S> findAll(@Nonnull Example<S> example, @Nonnull Pageable pageable);
44
+
45
+ boolean existsById(ID id);
46
+
47
+ long count();
48
+ }
@@ -0,0 +1,14 @@
1
+ package {{basePackage}}.databaseconfig;
2
+
3
+ import org.springframework.boot.persistence.autoconfigure.EntityScan;
4
+ import org.springframework.context.annotation.Configuration;
5
+ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
6
+ import org.springframework.transaction.annotation.EnableTransactionManagement;
7
+
8
+ @Configuration("databaseConfig")
9
+ @EnableJpaRepositories(basePackages = "{{basePackage}}.databaseconfig.dao")
10
+ @EnableTransactionManagement
11
+ @EntityScan(basePackages = "{{basePackage}}.databaseconfig.entity")
12
+ public class Config {
13
+ public static final String DATABASE_NAME = "{{projectNameKebab}}";
14
+ }
@@ -0,0 +1,9 @@
1
+ package {{basePackage}}.databaseconfig;
2
+
3
+ import org.springframework.context.annotation.Configuration;
4
+ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
5
+
6
+ @Configuration
7
+ @EnableJpaAuditing
8
+ public class JpaConfig {
9
+ }
@@ -0,0 +1,17 @@
1
+ configurations {
2
+ compileOnly {
3
+ extendsFrom annotationProcessor
4
+ }
5
+ }
6
+
7
+ dependencies {
8
+ api 'org.springframework.boot:spring-boot-starter-data-jpa'
9
+ api 'com.mysql:mysql-connector-j'
10
+ api 'org.projectlombok:lombok'
11
+ annotationProcessor 'org.projectlombok:lombok'
12
+ api 'com.fasterxml.jackson.core:jackson-databind'
13
+ api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
14
+
15
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
16
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
17
+ }
@@ -0,0 +1,9 @@
1
+ pluginManagement {
2
+ repositories {
3
+ gradlePluginPortal()
4
+ mavenCentral()
5
+ }
6
+ }
7
+
8
+ rootProject.name = '{{projectNameKebab}}'
9
+ include 'modules:{{projectNameKebab}}-config', 'modules:{{projectNameKebab}}-database-config'
@@ -0,0 +1,31 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import jakarta.servlet.http.HttpServletRequest;
4
+ import jakarta.servlet.http.HttpServletResponse;
5
+ import org.springframework.security.core.Authentication;
6
+ import org.springframework.security.oauth2.core.user.OAuth2User;
7
+ import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
8
+ import org.springframework.stereotype.Component;
9
+
10
+ /**
11
+ * OAuth2 登入成功後的處理器骨架。
12
+ *
13
+ * TODO:這裡只印出登入者資訊,沒有接你們的使用者體系(建立/查找內部帳號、發 JWT 等)。
14
+ * 這段一定要依專案實際的登入流程改寫,不要照抄。
15
+ *
16
+ * 這個檔案「不會」被自動掛進 SecurityConfig 的 filter chain ——
17
+ * SecurityConfig 每個專案客製化程度太高,birc-generator 不會自動改寫它,
18
+ * 請自行在 SecurityConfig 加上:
19
+ * .oauth2Login(oauth2 -> oauth2.successHandler(oAuth2LoginSuccessHandler))
20
+ */
21
+ @Component
22
+ public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
23
+
24
+ @Override
25
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
26
+ Authentication authentication) throws java.io.IOException {
27
+ OAuth2User user = (OAuth2User) authentication.getPrincipal();
28
+ // TODO: 用 user.getAttributes() 換內部使用者、發 JWT,這裡先示意
29
+ response.sendRedirect("/");
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ ---
2
+ # --- oauth2(由 birc-generator add 產生,範例先接 Google,其他 provider 自行調整 registration 區塊) ---
3
+ spring:
4
+ security:
5
+ oauth2:
6
+ client:
7
+ registration:
8
+ google:
9
+ client-id: ${OAUTH_GOOGLE_CLIENT_ID}
10
+ client-secret: ${OAUTH_GOOGLE_CLIENT_SECRET}
11
+ scope: email,profile
@@ -0,0 +1 @@
1
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
@@ -0,0 +1,23 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import io.swagger.v3.oas.annotations.OpenAPIDefinition;
4
+ import io.swagger.v3.oas.annotations.info.Info;
5
+ import io.swagger.v3.oas.annotations.security.SecurityScheme;
6
+ import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
7
+ import org.springframework.context.annotation.Configuration;
8
+
9
+ @Configuration
10
+ @OpenAPIDefinition(
11
+ info = @Info(
12
+ title = "{{projectName}} API",
13
+ version = "v1"
14
+ )
15
+ )
16
+ @SecurityScheme(
17
+ name = "bearerAuth",
18
+ type = SecuritySchemeType.HTTP,
19
+ scheme = "bearer",
20
+ bearerFormat = "JWT"
21
+ )
22
+ public class OpenApiConfig {
23
+ }
@@ -0,0 +1 @@
1
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:{{springdocVersion}}'
@@ -0,0 +1,76 @@
1
+ package {{basePackage}}.specification;
2
+
3
+ import jakarta.persistence.criteria.Path;
4
+ import jakarta.persistence.criteria.Predicate;
5
+ import java.util.Collection;
6
+ import java.util.List;
7
+ import java.util.Set;
8
+ import org.springframework.data.jpa.domain.Specification;
9
+
10
+ public class GenericSpecification<T> implements Specification<T> {
11
+
12
+ private final List<SearchCriteria> criteriaList;
13
+ private final Set<String> allowedFields;
14
+
15
+ public GenericSpecification(List<SearchCriteria> criteriaList) {
16
+ this(criteriaList, null);
17
+ }
18
+
19
+ public GenericSpecification(List<SearchCriteria> criteriaList, Set<String> allowedFields) {
20
+ this.criteriaList = criteriaList == null ? List.of() : criteriaList;
21
+ this.allowedFields = allowedFields;
22
+ }
23
+
24
+ @Override
25
+ public Predicate toPredicate(
26
+ jakarta.persistence.criteria.Root<T> root,
27
+ jakarta.persistence.criteria.CriteriaQuery<?> query,
28
+ jakarta.persistence.criteria.CriteriaBuilder cb) {
29
+ List<Predicate> predicates = criteriaList.stream()
30
+ .filter(this::isUsable)
31
+ .map(criteria -> toPredicate(root, cb, criteria))
32
+ .toList();
33
+ return cb.and(predicates.toArray(new Predicate[0]));
34
+ }
35
+
36
+ private boolean isUsable(SearchCriteria criteria) {
37
+ if (criteria == null || criteria.getField() == null || criteria.getField().isBlank()) {
38
+ return false;
39
+ }
40
+ if (allowedFields != null && !allowedFields.contains(criteria.getField())) {
41
+ return false;
42
+ }
43
+ return criteria.getOperator() == SearchCriteria.Operator.IS_NULL
44
+ || criteria.getOperator() == SearchCriteria.Operator.IS_NOT_NULL
45
+ || criteria.getValue() != null;
46
+ }
47
+
48
+ private Predicate toPredicate(
49
+ jakarta.persistence.criteria.Root<T> root,
50
+ jakarta.persistence.criteria.CriteriaBuilder cb,
51
+ SearchCriteria criteria) {
52
+ Path<?> path = resolvePath(root, criteria.getField());
53
+ return switch (criteria.getOperator()) {
54
+ case EQUAL -> cb.equal(path, criteria.getValue());
55
+ case NOT_EQUAL -> cb.notEqual(path, criteria.getValue());
56
+ case LIKE -> cb.like(path.as(String.class), "%" + criteria.getValue() + "%");
57
+ case GREATER_THAN -> cb.greaterThan(path.as(Comparable.class), (Comparable) criteria.getValue());
58
+ case GREATER_THAN_OR_EQUAL ->
59
+ cb.greaterThanOrEqualTo(path.as(Comparable.class), (Comparable) criteria.getValue());
60
+ case LESS_THAN -> cb.lessThan(path.as(Comparable.class), (Comparable) criteria.getValue());
61
+ case LESS_THAN_OR_EQUAL ->
62
+ cb.lessThanOrEqualTo(path.as(Comparable.class), (Comparable) criteria.getValue());
63
+ case IN -> path.in((Collection<?>) criteria.getValue());
64
+ case IS_NULL -> cb.isNull(path);
65
+ case IS_NOT_NULL -> cb.isNotNull(path);
66
+ };
67
+ }
68
+
69
+ private Path<?> resolvePath(jakarta.persistence.criteria.Root<T> root, String field) {
70
+ Path<?> path = root;
71
+ for (String part : field.split("\\.")) {
72
+ path = path.get(part);
73
+ }
74
+ return path;
75
+ }
76
+ }
@@ -0,0 +1,20 @@
1
+ package {{basePackage}}.dto;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Builder;
5
+ import lombok.Getter;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ @Getter
9
+ @Builder
10
+ @NoArgsConstructor
11
+ @AllArgsConstructor
12
+ public class PageInfo {
13
+ private int totalPage;
14
+ private long totalDataCount;
15
+ private int countPerPage;
16
+
17
+ public static PageInfo from(org.springframework.data.domain.Page<?> page) {
18
+ return new PageInfo(page.getTotalPages(), page.getTotalElements(), page.getSize());
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ package {{basePackage}}.dto;
2
+
3
+ import java.util.List;
4
+ import lombok.Getter;
5
+ import lombok.Setter;
6
+ import {{basePackage}}.specification.SearchCriteria;
7
+
8
+ @Getter
9
+ @Setter
10
+ public class PageRequest {
11
+
12
+ private int page = 1;
13
+ private int size = 20;
14
+ private String sortBy = "id";
15
+ private String sortDirection = "DESC";
16
+ private List<SearchCriteria> criteria;
17
+
18
+ public Pager toPager() {
19
+ return Pager.getInstance(page, size);
20
+ }
21
+
22
+ public org.springframework.data.domain.Pageable toPageable() {
23
+ org.springframework.data.domain.Sort sort = org.springframework.data.domain.Sort.by(
24
+ org.springframework.data.domain.Sort.Direction.fromString(sortDirection), sortBy);
25
+ return org.springframework.data.domain.PageRequest.of(Math.max(0, page - 1), size, sort);
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ package {{basePackage}}.dto;
2
+
3
+ import java.util.List;
4
+ import lombok.Getter;
5
+
6
+ @Getter
7
+ public class PageResponse<T> {
8
+
9
+ private final List<T> content;
10
+ private final int page;
11
+ private final int size;
12
+ private final long totalElements;
13
+ private final int totalPages;
14
+ private final PageInfo pageInfo;
15
+
16
+ public PageResponse(org.springframework.data.domain.Page<T> page) {
17
+ this.content = page.getContent();
18
+ this.page = page.getNumber() + 1;
19
+ this.size = page.getSize();
20
+ this.totalElements = page.getTotalElements();
21
+ this.totalPages = page.getTotalPages();
22
+ this.pageInfo = PageInfo.from(page);
23
+ }
24
+ }
@@ -0,0 +1,37 @@
1
+ package {{basePackage}}.dto;
2
+
3
+ import lombok.AccessLevel;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ @NoArgsConstructor
9
+ @AllArgsConstructor(access = AccessLevel.PRIVATE)
10
+ @Data
11
+ public class Pager {
12
+ private int page;
13
+ private int count;
14
+
15
+ public static Pager getInstance(int page, int count) {
16
+ return new Pager(page, count);
17
+ }
18
+
19
+ public static Pager infinity() {
20
+ return new Pager(-1, -1);
21
+ }
22
+
23
+ public boolean isInfinity() {
24
+ return page <= 0 || count <= 0;
25
+ }
26
+
27
+ public int getZeroBasedPage() {
28
+ return page > 0 ? page - 1 : 0;
29
+ }
30
+
31
+ public org.springframework.data.domain.Pageable toPageable() {
32
+ if (isInfinity()) {
33
+ return org.springframework.data.domain.Pageable.unpaged();
34
+ }
35
+ return org.springframework.data.domain.PageRequest.of(getZeroBasedPage(), count);
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ package {{basePackage}}.specification;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Getter;
5
+
6
+ @Getter
7
+ @AllArgsConstructor
8
+ public class SearchCriteria {
9
+
10
+ public enum Operator {
11
+ EQUAL,
12
+ NOT_EQUAL,
13
+ LIKE,
14
+ GREATER_THAN,
15
+ GREATER_THAN_OR_EQUAL,
16
+ LESS_THAN,
17
+ LESS_THAN_OR_EQUAL,
18
+ IN,
19
+ IS_NULL,
20
+ IS_NOT_NULL
21
+ }
22
+
23
+ private String field;
24
+ private Operator operator;
25
+ private Object value;
26
+ }
@@ -0,0 +1,34 @@
1
+ package {{basePackage}}.specification;
2
+
3
+ import org.springframework.data.jpa.domain.Specification;
4
+
5
+ /**
6
+ * 寫具名 XxxSpecification 時用:條件有值才加進 query,對齊 teaching-platform 的 filter 寫法。
7
+ */
8
+ public final class SpecificationSupport {
9
+
10
+ private SpecificationSupport() {
11
+ }
12
+
13
+ public static <T> Specification<T> conjunction() {
14
+ return (root, query, cb) -> cb.conjunction();
15
+ }
16
+
17
+ public static <T> Specification<T> equalIfPresent(String field, Object value) {
18
+ if (value == null || (value instanceof String text && text.isBlank())) {
19
+ return conjunction();
20
+ }
21
+ return (root, query, cb) -> cb.equal(root.get(field), value);
22
+ }
23
+
24
+ public static <T> Specification<T> likeIfPresent(String field, String value) {
25
+ if (value == null || value.isBlank()) {
26
+ return conjunction();
27
+ }
28
+ return (root, query, cb) -> cb.like(root.get(field), "%" + value + "%");
29
+ }
30
+
31
+ public static <T> Specification<T> and(Specification<T> left, Specification<T> right) {
32
+ return left.and(right);
33
+ }
34
+ }
@@ -0,0 +1,41 @@
1
+ package {{basePackage}}.aspect;
2
+
3
+ import {{basePackage}}.annotation.RequirePermission;
4
+ import org.aspectj.lang.JoinPoint;
5
+ import org.aspectj.lang.annotation.Aspect;
6
+ import org.aspectj.lang.annotation.Before;
7
+ import org.aspectj.lang.annotation.Pointcut;
8
+ import org.springframework.security.core.Authentication;
9
+ import org.springframework.security.core.context.SecurityContextHolder;
10
+ import org.springframework.stereotype.Component;
11
+
12
+ /**
13
+ * 攔截標了 @RequirePermission 的方法,檢查目前登入者是否有該權限代碼。
14
+ *
15
+ * TODO:這裡假設 Authentication 的 authorities 直接就是權限代碼字串,
16
+ * 實際專案通常是「使用者 -> 角色 -> 權限」這種多層對應,請依專案的權限模型改寫,
17
+ * 這只是示範攔截點該長在哪裡,不是完整的 RBAC 實作。
18
+ */
19
+ @Aspect
20
+ @Component
21
+ public class PermissionAspect {
22
+
23
+ @Pointcut("@annotation({{basePackage}}.annotation.RequirePermission)")
24
+ public void requirePermissionPointcut() {
25
+ }
26
+
27
+ @Before("requirePermissionPointcut()")
28
+ public void checkPermission(JoinPoint joinPoint) {
29
+ RequirePermission annotation = ((org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature())
30
+ .getMethod().getAnnotation(RequirePermission.class);
31
+
32
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
33
+ boolean hasPermission = authentication != null && authentication.getAuthorities().stream()
34
+ .anyMatch(a -> a.getAuthority().equals(annotation.value()));
35
+
36
+ if (!hasPermission) {
37
+ throw new org.springframework.security.access.AccessDeniedException(
38
+ "缺少權限: " + annotation.value());
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,18 @@
1
+ package {{basePackage}}.annotation;
2
+
3
+ import java.lang.annotation.ElementType;
4
+ import java.lang.annotation.Retention;
5
+ import java.lang.annotation.RetentionPolicy;
6
+ import java.lang.annotation.Target;
7
+
8
+ /**
9
+ * 標在 Controller 方法上,宣告呼叫這支 API 需要哪個權限代碼。
10
+ * 實際檢查邏輯在 PermissionAspect。
11
+ */
12
+ @Retention(RetentionPolicy.RUNTIME)
13
+ @Target(ElementType.METHOD)
14
+ public @interface RequirePermission {
15
+
16
+ /** 權限代碼,例如 "ACTIVITY_DELETE",實際命名規則請依專案的權限表設計 */
17
+ String value();
18
+ }
@@ -0,0 +1,2 @@
1
+ implementation 'org.springframework.boot:spring-boot-starter-security'
2
+ implementation 'org.springframework.boot:spring-boot-starter-aspectj'
@@ -0,0 +1,22 @@
1
+ package {{basePackage}}.schedule;
2
+
3
+ import lombok.extern.slf4j.Slf4j;
4
+ import org.springframework.scheduling.annotation.Scheduled;
5
+ import org.springframework.stereotype.Component;
6
+
7
+ /**
8
+ * 範例排程,示範怎麼寫、怎麼設定 cron。
9
+ *
10
+ * TODO:這只是範例,請改成實際的排程邏輯,或直接刪除這個檔案。
11
+ * cron 表達式建議寫在 application.yml,不要寫死在程式碼裡(見下方 ${schedule...} 用法),
12
+ * 方便不同環境(beta / online)用不同排程頻率。
13
+ */
14
+ @Slf4j
15
+ @Component
16
+ public class SampleSchedule {
17
+
18
+ @Scheduled(cron = "${schedule.sample.cron:0 0 * * * *}")
19
+ public void run() {
20
+ log.info("SampleSchedule 執行中...");
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import org.springframework.context.annotation.Configuration;
4
+ import org.springframework.scheduling.annotation.EnableScheduling;
5
+
6
+ /**
7
+ * 開啟 Spring 排程功能。實際的排程邏輯寫在 schedule/ 底下的類別,
8
+ * 用 @Scheduled 標記方法即可,不用另外註冊。
9
+ */
10
+ @Configuration
11
+ @EnableScheduling
12
+ public class SchedulingConfig {
13
+ }
@@ -0,0 +1,5 @@
1
+ ---
2
+ # --- scheduling(由 birc-generator add 產生) ---
3
+ schedule:
4
+ sample:
5
+ cron: "0 0 * * * *"
@@ -0,0 +1,74 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import {{basePackage}}.exception.ProjectException;
4
+ import io.sentry.SentryEvent;
5
+ import io.sentry.SentryLevel;
6
+ import io.sentry.SentryOptions;
7
+ import io.sentry.protocol.SentryException;
8
+ import org.springframework.context.annotation.Bean;
9
+ import org.springframework.context.annotation.Configuration;
10
+ import org.springframework.web.ErrorResponseException;
11
+
12
+ /**
13
+ * 過濾客戶端請求錯誤(HTTP 4xx),避免 Sentry 產生不需要處理的伺服器告警。
14
+ *
15
+ * ProjectException 子類依 {@code getHttpStatus()} 判斷,之後新增例外不必改這裡。
16
+ * Spring MVC / Bean Validation 則依套件前綴判斷。
17
+ */
18
+ @Configuration
19
+ public class SentryConfig {
20
+
21
+ @Bean
22
+ public SentryOptions.BeforeSendCallback beforeSendCallback() {
23
+ return (event, hint) -> {
24
+ if (event.getLevel() != SentryLevel.ERROR) {
25
+ return event;
26
+ }
27
+ if (shouldIgnore(event)) {
28
+ return null;
29
+ }
30
+ return event;
31
+ };
32
+ }
33
+
34
+ private static boolean shouldIgnore(SentryEvent event) {
35
+ for (Throwable current = event.getThrowable(); current != null; current = current.getCause()) {
36
+ if (isClientError(current)) {
37
+ return true;
38
+ }
39
+ }
40
+ if (event.getThrowable() != null || event.getExceptions() == null) {
41
+ return false;
42
+ }
43
+ for (SentryException exception : event.getExceptions()) {
44
+ if (isClientFrameworkType(exception.getType())) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ private static boolean isClientError(Throwable throwable) {
52
+ if (throwable instanceof ProjectException projectException) {
53
+ return projectException.getHttpStatus().is4xxClientError();
54
+ }
55
+ if (throwable instanceof ErrorResponseException errorResponseException) {
56
+ return errorResponseException.getStatusCode().is4xxClientError();
57
+ }
58
+ return isClientFrameworkType(throwable.getClass().getName());
59
+ }
60
+
61
+ private static boolean isClientFrameworkType(String type) {
62
+ if (type == null) {
63
+ return false;
64
+ }
65
+ return type.startsWith("jakarta.validation.")
66
+ || type.startsWith("org.springframework.web.bind.")
67
+ || type.startsWith("org.springframework.web.method.")
68
+ || type.startsWith("org.springframework.web.multipart.")
69
+ || type.startsWith("org.springframework.web.servlet.resource.")
70
+ || type.startsWith("org.springframework.http.converter.")
71
+ || "org.springframework.web.servlet.NoHandlerFoundException".equals(type)
72
+ || "org.springframework.web.HttpRequestMethodNotSupportedException".equals(type);
73
+ }
74
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ # --- sentry(由 birc-generator add 產生) ---
3
+ sentry:
4
+ dsn: ${SENTRY_DSN:}
5
+ send-default-pii: false
6
+ context-tags: [traceId, userId]
7
+ logs:
8
+ enabled: true
9
+ traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:0.1}
10
+ profile-session-sample-rate: ${SENTRY_PROFILE_SESSION_SAMPLE_RATE:0.1}
11
+ profile-lifecycle: TRACE
12
+ trace-propagation-targets:
13
+ - "localhost"
14
+ - "127.0.0.1"
15
+ exception-resolver-order: -2147483647
@@ -0,0 +1 @@
1
+ implementation 'io.sentry:sentry-spring-boot-4-starter:{{sentryVersion}}'
@@ -0,0 +1,9 @@
1
+ package {{basePackage}}.service;
2
+
3
+ import {{basePackage}}.dto.{{pascalCase entityName}}CreateRequest;
4
+ import {{basePackage}}.dto.{{pascalCase entityName}}Response;
5
+
6
+ public interface {{pascalCase entityName}}Service extends BaseService<{{pascalCase entityName}}CreateRequest, {{pascalCase entityName}}Response, Long> {
7
+
8
+ // 額外業務方法寫在這裡,例如 approve(Long id)。CRUD 不要重寫。
9
+ }
@@ -0,0 +1,23 @@
1
+ package {{basePackage}}.service.impl;
2
+
3
+ import org.springframework.stereotype.Service;
4
+ import {{basePackage}}.dto.{{pascalCase entityName}}CreateRequest;
5
+ import {{basePackage}}.dto.{{pascalCase entityName}}Response;
6
+ import {{basePackage}}.mapper.{{pascalCase entityName}}Mapper;
7
+ import {{basePackage}}.service.{{pascalCase entityName}}Service;
8
+ import {{daoPackage}}.{{pascalCase entityName}}DAO;
9
+ import {{entityPackage}}.{{pascalCase entityName}};
10
+
11
+ @Service
12
+ public class {{pascalCase entityName}}ServiceImpl
13
+ extends
14
+ BaseServiceImpl<{{pascalCase entityName}}, Long, {{pascalCase entityName}}CreateRequest, {{pascalCase entityName}}Response>
15
+ implements
16
+ {{pascalCase entityName}}Service {
17
+
18
+ public {{pascalCase entityName}}ServiceImpl(
19
+ {{pascalCase entityName}}DAO {{camelCase entityName}}DAO,
20
+ {{pascalCase entityName}}Mapper mapper) {
21
+ super({{camelCase entityName}}DAO, mapper);
22
+ }
23
+ }