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,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,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,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,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,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
|
+
}
|