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.
- package/.bircrc.example.json +14 -0
- package/AGENTS.md +20 -0
- package/PROJECT.md +83 -0
- package/README.md +300 -0
- package/bin/birc.js +645 -0
- package/bin/cli-util.js +428 -0
- package/bin/home-install.js +346 -0
- package/bin/update-check.js +153 -0
- package/implement.md +76 -0
- package/install.ps1 +156 -0
- package/install.sh +201 -0
- package/package.json +68 -0
- package/plopfile.js +1358 -0
- package/project-docs/AGENTS.md.hbs +12 -0
- package/project-docs/PROJECT.md.hbs +128 -0
- package/project-docs/implement.hbs +54 -0
- package/project-docs/test.hbs +31 -0
- package/scripts/install-bin.sh +72 -0
- package/scripts/update.sh +11 -0
- package/templates/aop/OperationLog.hbs +18 -0
- package/templates/aop/OperationLogAspect.hbs +95 -0
- package/templates/aop/build-gradle-dep.hbs +1 -0
- package/templates/base/Application.hbs +11 -0
- package/templates/base/ApplicationTests.java.hbs +11 -0
- package/templates/base/BaseService.hbs +15 -0
- package/templates/base/BaseServiceImpl.hbs +53 -0
- package/templates/base/EntityMapper.hbs +12 -0
- package/templates/base/README.md.hbs +56 -0
- package/templates/base/Result.hbs +48 -0
- package/templates/base/application-test.yml.hbs +27 -0
- package/templates/base/application.yml.hbs +18 -0
- package/templates/base/bircrc.hbs +14 -0
- package/templates/base/build.gradle.hbs +33 -0
- package/templates/base/gitignore.hbs +15 -0
- package/templates/client/ExternalApiClient.hbs +28 -0
- package/templates/client/ExternalApiClientConfig.hbs +26 -0
- package/templates/client/ExternalApiProperties.hbs +30 -0
- package/templates/client/application-yml-block.hbs +7 -0
- package/templates/controller.hbs +55 -0
- package/templates/dao.hbs +11 -0
- package/templates/docker/Dockerfile.hbs +29 -0
- package/templates/docker/docker-compose.prod.yml.hbs +65 -0
- package/templates/docker/docker-compose.yml.hbs +52 -0
- package/templates/docker/dockerignore.hbs +6 -0
- package/templates/docker/env.example.hbs +24 -0
- package/templates/dto-request.hbs +19 -0
- package/templates/dto-response.hbs +13 -0
- package/templates/email/EmailAsyncConfig.hbs +28 -0
- package/templates/email/EmailService.hbs +26 -0
- package/templates/email/EmailServiceImpl.hbs +49 -0
- package/templates/email/application-yml-block.hbs +19 -0
- package/templates/email/build-gradle-dep.hbs +2 -0
- package/templates/email/sample-template.hbs +16 -0
- package/templates/entity.hbs +46 -0
- package/templates/exception/ExceptionHandleController.hbs +16 -0
- package/templates/exception/NotFoundException.hbs +28 -0
- package/templates/exception/ProjectException.hbs +20 -0
- package/templates/exception.hbs +30 -0
- package/templates/file-upload/EmptyFileException.hbs +25 -0
- package/templates/file-upload/FileExtensionIllegalException.hbs +21 -0
- package/templates/file-upload/FileExtensionUtils.hbs +20 -0
- package/templates/file-upload/FileStorageProperties.hbs +37 -0
- package/templates/file-upload/FileStorageService.hbs +15 -0
- package/templates/file-upload/FileStorageServiceImpl.hbs +78 -0
- package/templates/file-upload/FileTooLargeException.hbs +21 -0
- package/templates/file-upload/FileUploadController.hbs +57 -0
- package/templates/file-upload/FileUtils.hbs +39 -0
- package/templates/file-upload/InvalidStoredFileException.hbs +21 -0
- package/templates/file-upload/application-yml-block.hbs +12 -0
- package/templates/gitlab-ci/gitlab-ci.hbs +158 -0
- package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/gradle-wrapper/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/templates/gradle-wrapper/gradlew +248 -0
- package/templates/gradle-wrapper/gradlew.bat +93 -0
- package/templates/log4j2/build-gradle-dep.hbs +6 -0
- package/templates/log4j2/log4j2-spring.xml.hbs +34 -0
- package/templates/mapper-gradle-dep.hbs +3 -0
- package/templates/mapper.hbs +11 -0
- package/templates/migration/add-column.hbs +4 -0
- package/templates/migration/create-table.hbs +9 -0
- package/templates/migration/custom.hbs +2 -0
- package/templates/migration/remove-column.hbs +4 -0
- package/templates/migration/seed.hbs +7 -0
- package/templates/multi-module/build.gradle.hbs +86 -0
- package/templates/multi-module/config/ApplicationConfig.java.hbs +33 -0
- package/templates/multi-module/config/Config.java.hbs +19 -0
- package/templates/multi-module/config/SecurityConfig.java.hbs +48 -0
- package/templates/multi-module/config/build.gradle.hbs +10 -0
- package/templates/multi-module/database-config/BaseDAO.java.hbs +31 -0
- package/templates/multi-module/database-config/BaseViewDAO.java.hbs +48 -0
- package/templates/multi-module/database-config/Config.java.hbs +14 -0
- package/templates/multi-module/database-config/JpaConfig.java.hbs +9 -0
- package/templates/multi-module/database-config/build.gradle.hbs +17 -0
- package/templates/multi-module/settings.gradle.hbs +9 -0
- package/templates/oauth/OAuth2LoginSuccessHandler.hbs +31 -0
- package/templates/oauth/application-yml-block.hbs +11 -0
- package/templates/oauth/build-gradle-dep.hbs +1 -0
- package/templates/openapi/OpenApiConfig.hbs +23 -0
- package/templates/openapi/build-gradle-dep.hbs +1 -0
- package/templates/pagination/GenericSpecification.hbs +76 -0
- package/templates/pagination/PageInfo.hbs +20 -0
- package/templates/pagination/PageRequest.hbs +27 -0
- package/templates/pagination/PageResponse.hbs +24 -0
- package/templates/pagination/Pager.hbs +37 -0
- package/templates/pagination/SearchCriteria.hbs +26 -0
- package/templates/pagination/SpecificationSupport.hbs +34 -0
- package/templates/permission/PermissionAspect.hbs +41 -0
- package/templates/permission/RequirePermission.hbs +18 -0
- package/templates/permission/build-gradle-dep.hbs +2 -0
- package/templates/scheduling/SampleSchedule.hbs +22 -0
- package/templates/scheduling/SchedulingConfig.hbs +13 -0
- package/templates/scheduling/application-yml-block.hbs +5 -0
- package/templates/sentry/SentryConfig.hbs +74 -0
- package/templates/sentry/application-yml-block.hbs +15 -0
- package/templates/sentry/build-gradle-dep.hbs +1 -0
- package/templates/service.hbs +9 -0
- package/templates/serviceImpl.hbs +23 -0
- package/templates/spotless/build-gradle-allprojects.hbs +16 -0
- package/templates/spotless/build-gradle-plugin.hbs +1 -0
- package/templates/spotless/spotless_formatter.xml +83 -0
- package/templates/sso/SsoAutoConfiguration.hbs +17 -0
- package/templates/sso/SsoProperties.hbs +27 -0
- package/templates/sso/application-yml-block.hbs +7 -0
- package/templates/valid-group/ValidGroup.hbs +20 -0
- package/test.md +56 -0
- package/uninstall.ps1 +60 -0
- package/uninstall.sh +76 -0
- 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,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,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,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,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
|
+
}
|