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,18 @@
1
+ spring:
2
+ application:
3
+ name: {{projectName}}
4
+ datasource:
5
+ url: ${DB_URL:jdbc:mysql://localhost:3306/app}
6
+ username: ${DB_USERNAME:root}
7
+ password: ${DB_PASSWORD:}
8
+ jpa:
9
+ hibernate:
10
+ ddl-auto: validate
11
+ open-in-view: false
12
+
13
+ server:
14
+ port: ${SPRING_SERVER_PORT:8080}
15
+
16
+ # 下面這行是 plop add 的插入點,不要刪、不要移動位置。
17
+ # 新的 feature 設定會被插到這行「之後」。
18
+ # birc-generator:config-anchor
@@ -0,0 +1,14 @@
1
+ {
2
+ "basePackage": "{{basePackage}}",
3
+ "projectNameKebab": "{{projectNameKebab}}",
4
+ "multiModule": {{multiModule}},
5
+ "srcPath": "src/main/java",
6
+ "resourcesPath": "src/main/resources",
7
+ "applicationYmlPath": "src/main/resources/application.yml",
8
+ "buildGradlePath": "build.gradle",
9
+ "entityPackage": "{{entityPackage}}",
10
+ "daoPackage": "{{daoPackage}}",
11
+ "entityPath": "{{entityPath}}",
12
+ "daoPath": "{{daoPath}}",
13
+ "features": {{{featuresJson}}}
14
+ }
@@ -0,0 +1,33 @@
1
+ plugins {
2
+ id 'java'
3
+ id 'org.springframework.boot' version '{{springBootVersion}}'
4
+ id 'io.spring.dependency-management' version '{{dependencyManagementVersion}}'
5
+ }
6
+
7
+ group = '{{basePackage}}'
8
+ version = '0.0.1-SNAPSHOT'
9
+
10
+ java {
11
+ sourceCompatibility = '21'
12
+ }
13
+
14
+ repositories {
15
+ mavenCentral()
16
+ }
17
+
18
+ dependencies {
19
+ // 下面這行是 plop add 的插入點,不要刪、不要移動位置。
20
+ // 新的 feature 依賴會被插到這行「之後」。
21
+ // birc-generator:dependency-anchor
22
+ implementation 'org.springframework.boot:spring-boot-starter-web'
23
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
24
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
25
+ compileOnly 'org.projectlombok:lombok'
26
+ annotationProcessor 'org.projectlombok:lombok'
27
+ runtimeOnly 'com.mysql:mysql-connector-j'
28
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
29
+ }
30
+
31
+ tasks.named('test') {
32
+ useJUnitPlatform()
33
+ }
@@ -0,0 +1,15 @@
1
+ build/
2
+ .gradle/
3
+ .env
4
+ upload/
5
+ uploads/
6
+ logs/
7
+ *.iml
8
+ .idea/
9
+ .classpath
10
+ .factorypath
11
+ .project
12
+ .settings/
13
+ .springBeans
14
+ .sts4-cache/
15
+ !gradle/wrapper/gradle-wrapper.jar
@@ -0,0 +1,28 @@
1
+ package {{basePackage}}.client;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ import lombok.extern.slf4j.Slf4j;
5
+ import org.springframework.stereotype.Component;
6
+ import org.springframework.web.client.RestClient;
7
+
8
+ /**
9
+ * 呼叫外部 API 的封裝範例,用 Spring 6 的 RestClient(同步、比 WebClient 簡單,
10
+ * 如果專案已經是 reactive 技術棧,改用 WebClient 會更一致)。
11
+ *
12
+ * TODO:這裡只示範一個 GET 方法,實際的 path、request/response DTO、
13
+ * 錯誤處理(4xx/5xx 轉成專案自己的例外)請依實際要接的 API 改寫。
14
+ */
15
+ @Slf4j
16
+ @Component
17
+ @RequiredArgsConstructor
18
+ public class ExternalApiClient {
19
+
20
+ private final RestClient externalApiRestClient;
21
+
22
+ public String ping() {
23
+ return externalApiRestClient.get()
24
+ .uri("/health")
25
+ .retrieve()
26
+ .body(String.class);
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+ import org.springframework.http.client.SimpleClientHttpRequestFactory;
7
+ import org.springframework.web.client.RestClient;
8
+
9
+ @Configuration
10
+ @RequiredArgsConstructor
11
+ public class ExternalApiClientConfig {
12
+
13
+ private final ExternalApiProperties properties;
14
+
15
+ @Bean
16
+ public RestClient externalApiRestClient() {
17
+ SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
18
+ requestFactory.setConnectTimeout(properties.getConnectTimeoutMs());
19
+ requestFactory.setReadTimeout(properties.getReadTimeoutMs());
20
+
21
+ return RestClient.builder()
22
+ .baseUrl(properties.getBaseUrl())
23
+ .requestFactory(requestFactory)
24
+ .build();
25
+ }
26
+ }
@@ -0,0 +1,30 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import jakarta.validation.constraints.Min;
4
+ import jakarta.validation.constraints.NotBlank;
5
+ import lombok.Getter;
6
+ import lombok.Setter;
7
+ import org.springframework.boot.context.properties.ConfigurationProperties;
8
+ import org.springframework.stereotype.Component;
9
+ import org.springframework.validation.annotation.Validated;
10
+
11
+ /**
12
+ * 外部 API 的連線設定,對應 application.yml 的 external.api.*。
13
+ * 一個專案如果要呼叫多個外部服務,複製這個類別改名即可,不用共用同一組設定。
14
+ */
15
+ @Getter
16
+ @Setter
17
+ @Validated
18
+ @Component
19
+ @ConfigurationProperties(prefix = "external.api")
20
+ public class ExternalApiProperties {
21
+
22
+ @NotBlank
23
+ private String baseUrl = "https://example.com";
24
+
25
+ @Min(1)
26
+ private int connectTimeoutMs = 3000;
27
+
28
+ @Min(1)
29
+ private int readTimeoutMs = 5000;
30
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ # --- client(由 birc-generator add 產生) ---
3
+ external:
4
+ api:
5
+ base-url: ${EXTERNAL_API_BASE_URL:https://example.com}
6
+ connect-timeout-ms: 3000
7
+ read-timeout-ms: 5000
@@ -0,0 +1,55 @@
1
+ package {{basePackage}}.controller;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ {{#if withExample}}
5
+ import org.springframework.validation.annotation.Validated;
6
+ import org.springframework.web.bind.annotation.DeleteMapping;
7
+ import org.springframework.web.bind.annotation.GetMapping;
8
+ import org.springframework.web.bind.annotation.PathVariable;
9
+ import org.springframework.web.bind.annotation.PostMapping;
10
+ import org.springframework.web.bind.annotation.PutMapping;
11
+ import org.springframework.web.bind.annotation.RequestBody;
12
+ {{/if}}
13
+ import org.springframework.web.bind.annotation.RequestMapping;
14
+ import org.springframework.web.bind.annotation.RestController;
15
+ {{#if withExample}}
16
+ import {{basePackage}}.validation.ValidGroup;
17
+ import {{basePackage}}.web.Result;
18
+ import {{basePackage}}.dto.{{pascalCase entityName}}CreateRequest;
19
+ import {{basePackage}}.dto.{{pascalCase entityName}}Response;
20
+ {{/if}}
21
+ import {{basePackage}}.service.{{pascalCase entityName}}Service;
22
+
23
+ @RestController
24
+ @RequestMapping("/api/{{lowerCase entityName}}s")
25
+ @RequiredArgsConstructor
26
+ public class {{pascalCase entityName}}Controller {
27
+
28
+ private final {{pascalCase entityName}}Service service;
29
+ {{#if withExample}}
30
+
31
+ @PostMapping
32
+ public Result<{{pascalCase entityName}}Response> create(
33
+ @Validated(ValidGroup.Create.class) @RequestBody {{pascalCase entityName}}CreateRequest request) {
34
+ return Result.success(service.create(request));
35
+ }
36
+
37
+ @GetMapping("/{id}")
38
+ public Result<{{pascalCase entityName}}Response> getById(@PathVariable Long id) {
39
+ return Result.success(service.getById(id));
40
+ }
41
+
42
+ @PutMapping("/{id}")
43
+ public Result<{{pascalCase entityName}}Response> update(
44
+ @PathVariable Long id,
45
+ @Validated(ValidGroup.Update.class) @RequestBody {{pascalCase entityName}}CreateRequest request) {
46
+ return Result.success(service.update(id, request));
47
+ }
48
+
49
+ @DeleteMapping("/{id}")
50
+ public Result<Void> delete(@PathVariable Long id) {
51
+ service.deleteById(id);
52
+ return Result.success();
53
+ }
54
+ {{/if}}
55
+ }
@@ -0,0 +1,11 @@
1
+ package {{daoPackage}};
2
+
3
+ import org.springframework.stereotype.Repository;
4
+ import {{entityPackage}}.{{pascalCase entityName}};
5
+
6
+ @Repository
7
+ public interface {{pascalCase entityName}}DAO extends BaseDAO<{{pascalCase entityName}}, Long> {
8
+ {{#if withExample}}
9
+ // 需要額外查詢時再在這裡加方法,例如 findByName
10
+ {{/if}}
11
+ }
@@ -0,0 +1,29 @@
1
+ # 多階段 build,用專案自己的 Gradle wrapper。
2
+ # 不以 build-arg / ARG 傳任何帳密:build args 會留在 image layer,
3
+ # 有 Harbor 拉取權的人用 docker history 就看得到。
4
+ FROM eclipse-temurin:21-jdk-alpine AS builder
5
+
6
+ WORKDIR /workspace
7
+ COPY gradlew .
8
+ COPY gradle gradle
9
+ COPY build.gradle settings.gradle ./
10
+ COPY modules/{{projectNameKebab}}-config/build.gradle modules/{{projectNameKebab}}-config/
11
+ COPY modules/{{projectNameKebab}}-database-config/build.gradle modules/{{projectNameKebab}}-database-config/
12
+ RUN chmod +x ./gradlew
13
+ RUN ./gradlew dependencies --no-daemon || true
14
+
15
+ COPY src src
16
+ COPY modules modules
17
+ RUN ./gradlew bootJar --no-daemon -x test
18
+
19
+ FROM eclipse-temurin:21-jre-alpine
20
+
21
+ WORKDIR /app
22
+ RUN addgroup -S spring && adduser -S spring -G spring \
23
+ && mkdir -p /app/upload && chown -R spring:spring /app/upload
24
+ COPY --from=builder /workspace/build/libs/*.jar app.jar
25
+ RUN chown spring:spring app.jar
26
+ USER spring:spring
27
+
28
+ EXPOSE 8080
29
+ ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "app.jar"]
@@ -0,0 +1,65 @@
1
+ services:
2
+ db:
3
+ image: mysql:8.4
4
+ container_name: {{dashCase projectName}}_db
5
+ restart: unless-stopped
6
+ environment:
7
+ MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
8
+ MYSQL_DATABASE: ${DB_DATABASE}
9
+ MYSQL_USER: ${DB_USER}
10
+ MYSQL_PASSWORD: ${DB_PASSWORD}
11
+ TZ: ${TZ:-Asia/Taipei}
12
+ volumes:
13
+ - type: volume
14
+ source: db_data
15
+ target: /var/lib/mysql
16
+ command:
17
+ - --character-set-server
18
+ - utf8mb4
19
+ - --collation-server
20
+ - utf8mb4_unicode_ci
21
+ healthcheck:
22
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
23
+ interval: 5s
24
+ timeout: 5s
25
+ retries: 10
26
+ logging:
27
+ driver: json-file
28
+ options:
29
+ max-size: "10m"
30
+ max-file: "3"
31
+
32
+ app:
33
+ image: ${DOCKER_IMAGE:-{{dashCase projectName}}-app}:${APP_TAG:-latest}
34
+ container_name: {{dashCase projectName}}_app
35
+ restart: unless-stopped
36
+ ports:
37
+ - target: 8080
38
+ published: ${SERVER_PORT:-8080}
39
+ protocol: tcp
40
+ environment:
41
+ DB_URL: jdbc:mysql://db:3306/${DB_DATABASE}
42
+ DB_USERNAME: ${DB_USER}
43
+ DB_PASSWORD: ${DB_PASSWORD}
44
+ TZ: ${TZ:-Asia/Taipei}
45
+ SPRING_PROFILES_ACTIVE: prod
46
+ FILE_STORAGE_PATH: /app/upload
47
+ SENTRY_DSN: ${SENTRY_DSN:-}
48
+ volumes:
49
+ - type: volume
50
+ source: app_uploads
51
+ target: /app/upload
52
+ depends_on:
53
+ db:
54
+ condition: service_healthy
55
+ logging:
56
+ driver: json-file
57
+ options:
58
+ max-size: "10m"
59
+ max-file: "3"
60
+
61
+ volumes:
62
+ db_data:
63
+ name: {{dashCase projectName}}_db_data
64
+ app_uploads:
65
+ name: {{dashCase projectName}}_app_uploads
@@ -0,0 +1,52 @@
1
+ services:
2
+ db:
3
+ image: mysql:5.7
4
+ container_name: {{dashCase projectName}}_db
5
+ restart: unless-stopped
6
+ environment:
7
+ MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
8
+ MYSQL_DATABASE: ${DB_DATABASE}
9
+ MYSQL_USER: ${DB_USER}
10
+ MYSQL_PASSWORD: ${DB_PASSWORD}
11
+ TZ: ${TZ:-Asia/Taipei}
12
+ ports:
13
+ - "${DB_PORT:-3306}:3306"
14
+ volumes:
15
+ - db_data:/var/lib/mysql
16
+ command:
17
+ - --character-set-server=utf8mb4
18
+ - --collation-server=utf8mb4_unicode_ci
19
+ healthcheck:
20
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_ROOT_PASSWORD}"]
21
+ interval: 5s
22
+ timeout: 5s
23
+ retries: 30
24
+ start_period: 180s
25
+
26
+ app:
27
+ build:
28
+ context: .
29
+ dockerfile: Dockerfile
30
+ image: ${DOCKER_IMAGE:-{{dashCase projectName}}-app}:dev
31
+ container_name: {{dashCase projectName}}_app
32
+ restart: unless-stopped
33
+ ports:
34
+ - "${SERVER_PORT:-8080}:8080"
35
+ # JDWP 沒有驗證,綁 loopback 只給本機 debugger 用,不對外開。
36
+ - "127.0.0.1:${DEBUG_PORT:-5005}:5005"
37
+ environment:
38
+ DB_URL: jdbc:mysql://db:3306/${DB_DATABASE}
39
+ DB_USERNAME: ${DB_USER}
40
+ DB_PASSWORD: ${DB_PASSWORD}
41
+ TZ: ${TZ:-Asia/Taipei}
42
+ SPRING_PROFILES_ACTIVE: dev
43
+ FILE_STORAGE_PATH: /app/upload
44
+ volumes:
45
+ - ./upload:/app/upload
46
+ depends_on:
47
+ db:
48
+ condition: service_healthy
49
+
50
+ volumes:
51
+ db_data:
52
+ name: {{dashCase projectName}}_db_data
@@ -0,0 +1,6 @@
1
+ build/
2
+ .gradle/
3
+ .git/
4
+ .idea/
5
+ *.iml
6
+ .env
@@ -0,0 +1,24 @@
1
+ # MySQL(docker compose up -d db)
2
+ # 密碼由產生器隨機生成,僅供本機開發;正式環境請自行更換。
3
+ DB_ROOT_PASSWORD={{dbRootPassword}}
4
+ DB_DATABASE={{snakeCase projectNameKebab}}
5
+ DB_USER={{snakeCase projectNameKebab}}_user
6
+ DB_PASSWORD={{dbPassword}}
7
+ DB_PORT=3306
8
+
9
+ # Spring Boot(bootRun 前先把 .env 載入目前 shell;Bash 用 source,PowerShell 見 README)
10
+ DB_URL=jdbc:mysql://127.0.0.1:3306/{{snakeCase projectNameKebab}}
11
+ DB_USERNAME={{snakeCase projectNameKebab}}_user
12
+ TZ=Asia/Taipei
13
+ SERVER_PORT=8080
14
+ DEBUG_PORT=5005
15
+ FILE_STORAGE_PATH=./upload
16
+ LOG_LEVEL=INFO
17
+ LOG_PATH=./logs
18
+
19
+ # 正式環境映像(docker-compose.prod.yml)
20
+ DOCKER_IMAGE={{snakeCase projectNameKebab}}_app
21
+ APP_TAG=latest
22
+
23
+ # 可選
24
+ SENTRY_DSN=
@@ -0,0 +1,19 @@
1
+ package {{basePackage}}.dto;
2
+ {{#if withExample}}
3
+
4
+ {{#each extraImports}}
5
+ import {{this}};
6
+ {{/each}}
7
+ import jakarta.validation.constraints.NotNull;
8
+ import {{basePackage}}.validation.ValidGroup;
9
+ {{/if}}
10
+
11
+ public record {{pascalCase entityName}}CreateRequest(
12
+ {{#if withExample}}
13
+ {{#each makeFields}}
14
+ @NotNull(groups = {ValidGroup.Create.class, ValidGroup.Update.class})
15
+ {{this.type}} {{this.name}}{{#unless @last}},{{/unless}}
16
+ {{/each}}
17
+ {{/if}}
18
+ ) {
19
+ }
@@ -0,0 +1,13 @@
1
+ package {{basePackage}}.dto;
2
+ {{#if withExample}}
3
+
4
+ {{#each extraImports}}
5
+ import {{this}};
6
+ {{/each}}
7
+ {{/if}}
8
+
9
+ public record {{pascalCase entityName}}Response(
10
+ Long id{{#if withExample}}{{#each makeFields}},
11
+ {{this.type}} {{this.name}}{{/each}}{{/if}}
12
+ ) {
13
+ }
@@ -0,0 +1,28 @@
1
+ package {{basePackage}}.config;
2
+
3
+ import java.util.concurrent.Executor;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+ import org.springframework.scheduling.annotation.EnableAsync;
7
+ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
8
+
9
+ /**
10
+ * 寄信專用的執行緒池,跟其他 @Async 用途分開,避免寄信卡住其他非同步工作,
11
+ * 反之亦然。如果專案已經有共用的 @EnableAsync 設定,把這個 @EnableAsync 拿掉即可,
12
+ * 一個專案只需要一個。
13
+ */
14
+ @Configuration
15
+ @EnableAsync
16
+ public class EmailAsyncConfig {
17
+
18
+ @Bean
19
+ public Executor mailExecutor() {
20
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
21
+ executor.setCorePoolSize(2);
22
+ executor.setMaxPoolSize(4);
23
+ executor.setQueueCapacity(50);
24
+ executor.setThreadNamePrefix("mail-");
25
+ executor.initialize();
26
+ return executor;
27
+ }
28
+ }
@@ -0,0 +1,26 @@
1
+ package {{basePackage}}.service;
2
+
3
+ import java.util.Map;
4
+
5
+ /**
6
+ * 郵件服務介面。
7
+ *
8
+ * 骨架由 birc-generator(plop add,選 email)產生。實際的寄信情境 (例如報名確認、活動取消通知)請依專案需求另外擴充方法,
9
+ * 不要把所有情境都塞進單一 send()。
10
+ */
11
+ public interface EmailService {
12
+
13
+ /**
14
+ * 寄送一封使用 Thymeleaf 模板渲染內容的郵件。
15
+ *
16
+ * @param to
17
+ * 收件人
18
+ * @param subject
19
+ * 主旨
20
+ * @param templateName
21
+ * 對應 src/main/resources/email/ 底下的模板檔名(不含 .html)
22
+ * @param variables
23
+ * 模板變數
24
+ */
25
+ void send(String to, String subject, String templateName, Map<String, Object> variables);
26
+ }
@@ -0,0 +1,49 @@
1
+ package {{basePackage}}.service.impl;
2
+
3
+ import jakarta.mail.internet.MimeMessage;
4
+ import java.util.Map;
5
+ import lombok.RequiredArgsConstructor;
6
+ import lombok.extern.slf4j.Slf4j;
7
+ import org.springframework.mail.javamail.JavaMailSender;
8
+ import org.springframework.mail.javamail.MimeMessageHelper;
9
+ import org.springframework.scheduling.annotation.Async;
10
+ import org.springframework.stereotype.Service;
11
+ import org.thymeleaf.context.Context;
12
+ import org.thymeleaf.spring6.SpringTemplateEngine;
13
+ import {{basePackage}}.service.EmailService;
14
+
15
+ /**
16
+ * 郵件服務實作。
17
+ *
18
+ * 寄信是 @Async,透過同時產生的 EmailAsyncConfig 裡定義的 mailExecutor 執行緒池送出, 呼叫端不會被阻塞;失敗只記
19
+ * log,不往外拋,避免寄信失敗連帶讓主流程 rollback。 這個行為跟寄信本身該不該擋住主流程有關,如果你的情境是「寄信失敗要讓操作失敗」,
20
+ * 請不要照抄這個 catch-and-log 寫法。
21
+ */
22
+ @Slf4j
23
+ @Service
24
+ @RequiredArgsConstructor
25
+ public class EmailServiceImpl implements EmailService {
26
+
27
+ private final JavaMailSender mailSender;
28
+ private final SpringTemplateEngine templateEngine;
29
+
30
+ @Override
31
+ @Async("mailExecutor")
32
+ public void send(String to, String subject, String templateName, Map<String, Object> variables) {
33
+ try {
34
+ Context context = new Context();
35
+ context.setVariables(variables);
36
+ String html = templateEngine.process("email/" + templateName, context);
37
+
38
+ MimeMessage message = mailSender.createMimeMessage();
39
+ MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
40
+ helper.setTo(to);
41
+ helper.setSubject(subject);
42
+ helper.setText(html, true);
43
+
44
+ mailSender.send(message);
45
+ } catch (Exception e) {
46
+ log.error("寄信失敗,to={}, template={}, error={}", to, templateName, e.getMessage());
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ # --- email(由 birc-generator add 產生,環境變數請放 .env) ---
3
+ # 用 --- 開新的 YAML document,避免跟既有的 spring: 區塊在同一份 document 裡重複 key
4
+ # (SnakeYAML 對同一 document 內重複的 top-level key 是後蓋前,不會自動合併)
5
+ spring:
6
+ mail:
7
+ host: ${MAIL_HOST:smtp.gmail.com}
8
+ port: ${MAIL_PORT:587}
9
+ username: ${MAIL_USERNAME}
10
+ password: ${MAIL_PASSWORD}
11
+ properties:
12
+ mail:
13
+ smtp:
14
+ auth: true
15
+ starttls:
16
+ enable: true
17
+ birc:
18
+ mail:
19
+ admin-email: ${MAIL_ADMIN_EMAIL:admin@example.com}
@@ -0,0 +1,2 @@
1
+ implementation 'org.springframework.boot:spring-boot-starter-mail'
2
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html xmlns:th="http://www.thymeleaf.org">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title th:text="${subject}">通知信</title>
6
+ </head>
7
+ <body style="font-family: sans-serif; line-height: 1.6;">
8
+ <p th:text="${greeting} ?: '您好,'"></p>
9
+ <p>
10
+ 這是 birc-generator 產生的範例模板,實際內容請自行修改。
11
+ 變數用 Thymeleaf 語法帶入,例如 <code>th:text="${activityName}"</code>。
12
+ </p>
13
+ <hr />
14
+ <p style="color: #888; font-size: 12px;">此為系統自動發送信件,請勿直接回覆。</p>
15
+ </body>
16
+ </html>
@@ -0,0 +1,46 @@
1
+ package {{entityPackage}};
2
+
3
+ {{#each entityImports}}
4
+ import {{this}};
5
+ {{/each}}
6
+ import jakarta.persistence.Entity;
7
+ import jakarta.persistence.GeneratedValue;
8
+ import jakarta.persistence.GenerationType;
9
+ import jakarta.persistence.Id;
10
+ {{#if withExample}}
11
+ import jakarta.persistence.Column;
12
+ {{/if}}
13
+ import jakarta.persistence.Table;
14
+ {{#if withSoftDelete}}
15
+ import org.hibernate.annotations.SoftDelete;
16
+ import org.hibernate.annotations.SoftDeleteType;
17
+ {{/if}}
18
+ import lombok.Getter;
19
+ import lombok.Setter;
20
+
21
+ @Entity
22
+ {{#if withSoftDelete}}
23
+ @SoftDelete(columnName = "deleted_at", strategy = SoftDeleteType.TIMESTAMP)
24
+ {{/if}}
25
+ @Table(name = "{{snakeCase entityName}}")
26
+ @Getter
27
+ @Setter
28
+ public class {{pascalCase entityName}} {
29
+
30
+ @Id
31
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
32
+ private Long id;
33
+ {{#if withExample}}
34
+ {{#each makeFields}}
35
+
36
+ @Column(name = "{{this.snakeName}}")
37
+ private {{this.type}} {{this.name}};
38
+ {{/each}}
39
+
40
+ @Column(name = "created_at", insertable = false, updatable = false)
41
+ private LocalDateTime createdAt;
42
+
43
+ @Column(name = "updated_at", insertable = false, updatable = false)
44
+ private LocalDateTime updatedAt;
45
+ {{/if}}
46
+ }
@@ -0,0 +1,16 @@
1
+ package {{basePackage}}.controller;
2
+
3
+ import org.springframework.http.ResponseEntity;
4
+ import org.springframework.web.bind.annotation.ExceptionHandler;
5
+ import org.springframework.web.bind.annotation.RestControllerAdvice;
6
+ import {{basePackage}}.exception.ProjectException;
7
+ import {{basePackage}}.web.Result;
8
+
9
+ @RestControllerAdvice
10
+ public class ExceptionHandleController {
11
+
12
+ @ExceptionHandler(ProjectException.class)
13
+ public ResponseEntity<Result<Void>> handleProjectException(ProjectException e) {
14
+ return ResponseEntity.status(e.getHttpStatus()).body(Result.failure(e.getErrorCode(), e.getMessage()));
15
+ }
16
+ }