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,12 @@
1
+ # {{projectName}} — Agent 目錄
2
+
3
+ BIRC 產出的 Spring Boot 專案。人跟 AI coding agent 先讀這份,再只打開這次用得到的那一份。
4
+
5
+ | 文件 | 何時讀 |
6
+ | --- | --- |
7
+ | [ponytail](https://github.com/DietrichGebert/ponytail/blob/main/skills/ponytail/SKILL.md) | 實作時決定**做多大**。這才是 implement 的手感。專案裡的梯子寫在 [PROJECT.md](PROJECT.md) 的「夠用就停」。 |
8
+ | [PROJECT.md](PROJECT.md) | 架構、分層、CRUD 慣例、opt-in。規範只活在這裡。 |
9
+ | [implement.md](implement.md) | 計畫已定,要把功能做成 diff。 |
10
+ | [test.md](test.md) | 開始寫或改測試。 |
11
+
12
+ 功能模組(email / sso / oauth)用 `birc add`,不要從別的專案抄設定。
@@ -0,0 +1,128 @@
1
+ # {{projectName}} — 專案架構與規範
2
+
3
+ ## 夠用就停
4
+
5
+ 先讀這次要動的程式與資料流,再決定要不要寫新的。從上往下,停在第一個站得住的:
6
+
7
+ 1. 這次真的要做嗎?推測以後用得到的先不做。
8
+ 2. 專案裡已經有了嗎?繼承 `BaseService`,用現成 Mapper / DAO,功能模組走 `birc add`。
9
+ 3. Spring、Jakarta、JDK 已經有了嗎?驗證、MapStruct、`Specification` 用現成的,不要自製框架。
10
+ 4. `build.gradle` 裡的依賴夠不夠?不要為了對稱再加套件。
11
+ 5. 只缺一個行為?在 `XxxService` 加方法,不要新 class、新資料夾。
12
+ 6. 以上都不夠,才寫最小能跑的。
13
+
14
+ 驗證、權限、資料不能丟的錯誤處理不算「可以少寫」。CRUD 繼承是第 2 階的預設答案。
15
+
16
+ ## 預設節奏:CRUD 繼承,方法外加
17
+
18
+ 標準增刪改查只寫在 `BaseServiceImpl`。`XxxService` 繼承 `BaseService<CreateRequest, Response, Long>`,不要把 create / getById / update / deleteById 再抄一次。多出來的業務(送審、審核、統計)才在 `XxxService` 加方法,在 `XxxServiceImpl` 實作,直接用繼承下來的 `dao`、`mapper`。
19
+
20
+ 資料流:
21
+
22
+ ```
23
+ HTTP JSON → XxxCreateRequest → Mapper → Entity → DAO
24
+ DB → Entity → Mapper → XxxResponse → Result<T>
25
+ ```
26
+
27
+ 新增一張表用 `birc make`。先讓 CRUD 能跑;有規則再加方法。不要為了彈性先上 Clean Architecture 資料夾或一個操作一個 UseCase class。
28
+
29
+ ## 技術棧
30
+
31
+ - Spring Boot 4 / Java 21
32
+ - Spring Data JPA + MySQL
33
+ - MapStruct(DTO 轉換)
34
+
35
+ ## 目錄結構
36
+
37
+ (依 `.bircrc.json` 的 `basePackage` 展開)
38
+
39
+ - `entity/`(多模組時在 `databaseconfig.entity`)
40
+ - `dao/`(`XxxDAO extends BaseDAO`;多模組時在 `databaseconfig.dao`)
41
+ - `dto/`(`XxxCreateRequest` / `XxxResponse`)
42
+ - `mapper/`(`EntityMapper`;`XxxMapper extends EntityMapper`)
43
+ - `service/`、`service/impl/`(`BaseService`;`XxxService extends BaseService`)
44
+ - `controller/`
45
+ - `web/Result`
46
+
47
+ ## 分層(baseline)
48
+
49
+ - `entity`:JPA,多模組時在 `databaseconfig`。只放欄位與 getter/setter。沒有 `BaseEntity`。
50
+ - `dto`:Java record。`XxxCreateRequest` 進來(`jakarta.validation` + `ValidGroup.Create` / `ValidGroup.Update`),`XxxResponse` 出去。這是 API 與 Service 的語言,不是 Entity。
51
+ - `mapper`:`XxxMapper extends EntityMapper<Entity, CreateRequest, Response>`,MapStruct、`componentModel = "spring"`。跨欄位計算才寫 `default method`。轉換一律走 Mapper,不走 `JavaBeanUtils`、不手寫 Transformer。
52
+ - `dao`:`XxxDAO extends BaseDAO<Entity, ID>`。自訂查詢才在介面加方法。資料庫 VIEW 不能寫入時,那個 DAO `extends BaseViewDAO`(沒有 save / delete)。不要為了跟 DAO 對稱再做一個 `BaseViewService`。
53
+ - `service`:Controller 注入 `XxxService` 介面。一對一 `XxxServiceImpl` 是正常的;介面的價值是額外方法的掛點與 Spring 注入,不是預先準備第二套實作。查詢 API 大到該獨立時,另開一個查詢介面(例如只含 search / count),不要把八十個方法塞進同一個 Service。
54
+ - `controller`:參數驗證與呼叫 Service。回傳專案內 `web.Result<T>`。不回 Entity、不注入 DAO、不引用 `birc-spring-boot-starter`。
55
+
56
+ ## 統一規範
57
+
58
+ - 例外丟 `ProjectException` 子類(例如 `NotFoundException`),由 `ExceptionHandleController` 轉成回應。Controller 不手動 try-catch 包錯誤訊息。
59
+ - 整組骨架用 `birc make`;單層用 `birc make:entity` / `make:model` / `make:mapper` / `make:service` / `make:controller` / `make:exception`。`make:entity` 只生 Entity 與 DAO;`--dto` 與 `--mapper` 一併生 Mapper 與 DTO。`make:model` 可加 `--migration` / `--seed` / `--controller`。`--soft-delete` 用 Hibernate `@SoftDelete` 的 `deleted_at`,查詢自動排除已刪;不要自寫 `@SQLDelete`。預設是初稿,完整範例加 `--example`。不要複製其他 Entity 的程式碼來改名。
60
+ - 欄位驗證用專案內 `validation/ValidGroup`。CRUD 用 `Create` / `Update`。`Delete` 給「刪除還要帶原因、確認碼、樂觀鎖 version」的 body。`Submit` 給草稿可少填、送審要全填(活動送審就是這組)。Controller 的 `@Validated` 與欄位 `groups` 對同一組。
61
+ - email / sso / oauth / validGroup 用 `birc add`,不要從別的專案抄設定。
62
+ - 格式化走 Spotless(`./gradlew spotlessApply`),不要另裝 Checkstyle。
63
+
64
+ ## 已啟用的功能模組
65
+
66
+ 見 `.bircrc.json` 的 `features` 欄位。要加新的模組用 `birc add`。
67
+
68
+ ## SSO / Sentry
69
+
70
+ 在 `application.yml` 設定:
71
+
72
+ ```yaml
73
+ birc:
74
+ sso:
75
+ enabled: true
76
+ login-url: ...
77
+ validate-url: ...
78
+ ```
79
+
80
+ 沒有 SSO 的專案就不用設定這段,`SsoAutoConfiguration` 不會被啟用。
81
+
82
+ ## 進階模式(opt-in)
83
+
84
+ 只在對應條件出現時才用。判斷不出來就停在 baseline,之後再重構。這是「夠用就停」的第 6 階,不是每個 entity 的起手式。
85
+
86
+ ### 1. 把邏輯搬進 Entity(rich entity)
87
+
88
+ **何時:** 有明確狀態機,且轉換規則會被多個 service 方法重複檢查。
89
+
90
+ **怎麼做:** 做成 `activity.approve()`、`activity.reject(String reason)` 這類方法,由 entity 檢查是否允許轉換。允許關係收成一張表,不要散成 `if/else`。`ServiceImpl` 只做 load、權限、呼叫 entity、save。只套在真的有狀態機的 entity。
91
+
92
+ ### 2. Domain Event
93
+
94
+ **何時:** 成功後要做跟核心無關、失敗也不該讓主流程失敗的事(發信、稽核)。
95
+
96
+ **怎麼做:** `ApplicationEventPublisher.publishEvent(...)`,監聽用 `@TransactionalEventListener(phase = AFTER_COMMIT)`。主流程裡用 try-catch 包住通知來「避免發信失敗」就是該改成 event 的訊號。
97
+
98
+ ### 3. Value Object
99
+
100
+ **何時:** 一組欄位有自己的驗證,且多處重複(email 格式、日期區間)。
101
+
102
+ **怎麼做:** 獨立 record,建構子裡驗證失敗就丟例外。沿用 DTO 用 record 的慣例。純顯示字串不必包。
103
+
104
+ ### 4. Specification
105
+
106
+ **何時:** DAO 出現大量 `findByXAndYAndZ` 名稱組合爆炸。
107
+
108
+ **怎麼做:** Spring Data `Specification<T>`,條件拆成靜態方法,在 Service 組合。
109
+
110
+ ### 5. Port / Adapter
111
+
112
+ **何時:** 串專案外部系統(第三方 API、SSO、檔案儲存),不是資料庫。
113
+
114
+ **怎麼做:** Service 只看 interface;實作放 `client` 或對應 infra。資料庫繼續用 `BaseDAO`,不必再包一層 repository port。
115
+
116
+ ## 禁止事項
117
+
118
+ - 業務規則失敗繼續丟 `ProjectException` 子類,不要另做 `Either` / `Result<T, E>` 兩套錯誤處理。
119
+ - Entity 上的業務方法只在「進階模式 1」成立時才加。
120
+ - Controller 不直接注入 DAO、不回傳 Entity。
121
+ - 不把 Transformer / `JavaBeanUtils.copy` 加回來。
122
+ - 不為了對稱新增 `BaseViewService`、`BaseEntity`,也不把 Clean Architecture 四層當每個 CRUD 的起手式。
123
+
124
+ ## 待辦 / 已知限制
125
+
126
+ (這裡列目前專案的 TODO 或已知技術債,讓後續接手的人或 AI agent 一看就懂現狀,不用重新問一次)
127
+
128
+ - [ ]
@@ -0,0 +1,54 @@
1
+ # implement
2
+
3
+ 把**已經決定**的工作做成可合併的 diff。不重開計畫。
4
+
5
+ ## 跟其它文件
6
+
7
+ | 先讀 | 再做 |
8
+ | --- | --- |
9
+ | ponytail + [PROJECT.md](PROJECT.md) 的夠用就停 | 決定這次寫哪些檔。手感在梯子,不在這份。 |
10
+ | [PROJECT.md](PROJECT.md) | 對齊分層與慣例。 |
11
+ | [test.md](test.md) | 步驟走到「寫測試」時打開。 |
12
+
13
+ ## 何時用
14
+
15
+ 人叫「照 implement 做」,或把已定案的票丟過來。計畫還不存在就先問。
16
+
17
+ ## 步驟
18
+
19
+ 1. 讀已定案行為。公開邊界通常是 `XxxService`、Controller 的 HTTP 契約、`Result` 外殼。
20
+ 2. 爬夠用就停:能繼承 `BaseService`、能 `birc add`、能在現有方法加行為,就停在那一階。
21
+ 3. 依 [test.md](test.md) 一條紅 → 剛好夠綠 → 下一條。
22
+ 4. 最後跑一次 `./gradlew test`(Windows `gradlew.bat test`)。
23
+ 5. 人要求 commit、或這次明確說「做完並提交」才提交。格式見下面。沒說就停在回報。
24
+
25
+ 完成條件:能說出停在梯子第幾階;`./gradlew test` 綠;沒有順便新資料夾或第二套錯誤處理。
26
+
27
+ ## Commit
28
+
29
+ Commit 前跑 `./gradlew spotlessApply`(Windows `gradlew.bat spotlessApply`)。
30
+
31
+ Conventional Commits,標題與本文一律中文:
32
+
33
+ - 類型:`feat:` / `fix:` / `refactor:` / `chore:` / `test:` / `docs:`
34
+ - 標題寫為什麼,一行。本文用編號列做了什麼。
35
+ - 不要加 `Ultraworked by AI`、`Co-authored-by:` 或其它 AI 後綴。
36
+
37
+ ```text
38
+ feat: 新增使用者驗證功能
39
+
40
+ 1. 使用 JWT 實作 Token 管理
41
+ 2. 加入密碼強度驗證 (Model Validator)
42
+ 3. 補充 Unit Tests,覆蓋率達 85%
43
+ ```
44
+
45
+ ## 回報
46
+
47
+ ```text
48
+ 梯子停在: …
49
+ 測試檔: …
50
+ 圍住: …
51
+ 人工突變: 改了 X,Y 測試轉紅
52
+ 仍需人看: …
53
+ 跳過: …
54
+ ```
@@ -0,0 +1,31 @@
1
+ # test
2
+
3
+ 這個 Spring 專案裡測試怎麼寫。流程在 [implement.md](implement.md);規範在 [PROJECT.md](PROJECT.md)。
4
+
5
+ 目標不是「讓測試變綠」,是「有問題的碼過不了關」。
6
+
7
+ ## 工具
8
+
9
+ 沿用專案已有的 `./gradlew test`(JUnit、`spring-boot-starter-test`)。不要為了某一張票引入 Cucumber、PITest,除非票本身就是加那道門檻。
10
+
11
+ Given-When-Then 寫在**測試方法名稱**,例如 `create_whenEmailMissing_rejectsWithValidGroupCreate`。
12
+
13
+ ## 打在哪
14
+
15
+ 優先打 `XxxService` 的公開方法:CRUD 繼承來的不用再抄一遍;多出來的業務(送審、審核、名額)才測。Controller 只在 HTTP 契約或 `ValidGroup` 分組是這次改動時才測。
16
+
17
+ 不斷言 Mapper 的欄位對欄位清單,除非轉換本身有跨欄位規則。預期值來自規格,不要用實作自己算一遍。
18
+
19
+ 每條關鍵測試要寫得出「這條防住什麼」。
20
+
21
+ ## 關鍵路徑 vs 低風險
22
+
23
+ 關鍵:狀態轉換、名額/計數、權限、錢、取消後要精確回退。這類用**業務不變量**斷言(不得為負、不得超過上限、回滾後與事前相同),並附反向案例(狀態不允許、權限不足要丟對應的 `ProjectException` 子類)。
24
+
25
+ 低風險:純 getter、空的初稿 Controller、只改文案。零測試或一條鎖定契約即可。
26
+
27
+ 有共享計數(報名、庫存)才測競態。時間邊界注入可控時鐘,不用 `Thread.sleep`。
28
+
29
+ 至少一次**人工突變**:改壞剛寫的關鍵規則,確認測試轉紅,再改回來。
30
+
31
+ 完成條件:這次改動的關鍵路徑有正反案例;低風險沒被拿去湊數量。
@@ -0,0 +1,72 @@
1
+ # 給 install.sh / update.sh source。把 birc 掛到使用者寫得到的 bin,不呼叫 sudo。
2
+
3
+ birc_is_windows() {
4
+ [ -n "${WINDIR:-}" ] || [[ "${OSTYPE:-}" == msys* || "${OSTYPE:-}" == cygwin* || "${OSTYPE:-}" == mingw* ]]
5
+ }
6
+
7
+ birc_try_bin_dir() {
8
+ local dir="$1"
9
+ [ -n "$dir" ] || return 1
10
+ mkdir -p "$dir" 2>/dev/null || return 1
11
+ [ -w "$dir" ] || return 1
12
+ printf '%s\n' "$dir"
13
+ }
14
+
15
+ birc_detect_bin_dir() {
16
+ local npm_prefix="" npm_bin=""
17
+ npm_prefix="$(npm config get prefix 2>/dev/null || true)"
18
+ if [ -n "$npm_prefix" ]; then
19
+ if [ -d "$npm_prefix/bin" ]; then
20
+ npm_bin="$npm_prefix/bin"
21
+ else
22
+ npm_bin="$npm_prefix"
23
+ fi
24
+ fi
25
+
26
+ if birc_is_windows; then
27
+ birc_try_bin_dir "$npm_bin" && return 0
28
+ birc_try_bin_dir "$HOME/.local/bin" && return 0
29
+ else
30
+ birc_try_bin_dir /usr/local/bin && return 0
31
+ birc_try_bin_dir "$npm_bin" && return 0
32
+ birc_try_bin_dir "$HOME/.local/bin" && return 0
33
+ fi
34
+
35
+ error "找不到可寫入的 bin 目錄。請先確認 npm 可用,或自行把 $HOME/.local/bin 加進 PATH。"
36
+ }
37
+
38
+ birc_install_command() {
39
+ local script_path="$1"
40
+ local bin_dir escaped win_path
41
+ bin_dir="$(birc_detect_bin_dir)"
42
+ escaped="${script_path//\'/\'\\\'\'}"
43
+
44
+ cat > "$bin_dir/birc" <<EOF
45
+ #!/usr/bin/env bash
46
+ _birc_js='$escaped'
47
+ if command -v cygpath >/dev/null 2>&1; then
48
+ exec node "\$(cygpath -w "\$_birc_js")" "\$@"
49
+ fi
50
+ exec node "\$_birc_js" "\$@"
51
+ EOF
52
+ chmod +x "$bin_dir/birc"
53
+
54
+ if birc_is_windows; then
55
+ if command -v cygpath >/dev/null 2>&1; then
56
+ win_path="$(cygpath -w "$script_path")"
57
+ else
58
+ win_path="$script_path"
59
+ fi
60
+ printf '@echo off\nnode "%s" %%*\n' "$win_path" > "$bin_dir/birc.cmd"
61
+ fi
62
+
63
+ info "已安裝指令: $bin_dir/birc"
64
+ case ":$PATH:" in
65
+ *":$bin_dir:"*) ;;
66
+ *)
67
+ export PATH="$bin_dir:$PATH"
68
+ warn "已暫時把 $bin_dir 加進 PATH。若關掉終端後找不到 birc,把下面這行加進 ~/.bashrc:"
69
+ warn "export PATH=\"$bin_dir:\$PATH\""
70
+ ;;
71
+ esac
72
+ }
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # 相容入口。實際更新改走 Node,讓 PowerShell 裝的 Windows 不必再找 bash。
5
+ # 由 checkout 執行: bash scripts/update.sh
6
+ # `birc update` 直接 require bin/home-install.js。
7
+
8
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ export BIRC_CURRENT_ROOT="${BIRC_CURRENT_ROOT:-$ROOT}"
10
+ export BIRC_INSTALL_DIR="${BIRC_INSTALL_DIR:-$HOME/.birc-generator}"
11
+ exec node "$ROOT/bin/home-install.js" update
@@ -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 / Service 方法上,用來記錄操作事件(誰、做了什麼、對哪筆資料)。
10
+ * 實際落地方式(存 DB / 送 log 系統)在 OperationLogAspect 裡實作,這裡先印 log。
11
+ */
12
+ @Retention(RetentionPolicy.RUNTIME)
13
+ @Target(ElementType.METHOD)
14
+ public @interface OperationLog {
15
+
16
+ /** 操作說明,例如 "建立活動"、"刪除使用者" */
17
+ String value();
18
+ }
@@ -0,0 +1,95 @@
1
+ package {{basePackage}}.aspect;
2
+
3
+ import java.util.Arrays;
4
+ import lombok.extern.slf4j.Slf4j;
5
+ import org.aspectj.lang.JoinPoint;
6
+ import org.aspectj.lang.annotation.AfterReturning;
7
+ import org.aspectj.lang.annotation.AfterThrowing;
8
+ import org.aspectj.lang.annotation.Aspect;
9
+ import org.aspectj.lang.annotation.Pointcut;
10
+ import org.aspectj.lang.reflect.MethodSignature;
11
+ import org.springframework.beans.factory.annotation.Value;
12
+ import org.springframework.stereotype.Component;
13
+ import org.springframework.web.context.request.RequestContextHolder;
14
+ import org.springframework.web.context.request.ServletRequestAttributes;
15
+ import {{basePackage}}.annotation.OperationLog;
16
+
17
+ @Slf4j
18
+ @Aspect
19
+ @Component
20
+ public class OperationLogAspect {
21
+
22
+ // X-Forwarded-User 是客戶端可偽造的標頭,只有部署在會覆寫此標頭的
23
+ // 可信反向代理(如 SSO gateway)後面才允許採信,預設關閉。
24
+ @Value("${operation-log.trust-forwarded-user:false}")
25
+ private boolean trustForwardedUser;
26
+
27
+ @Pointcut("@annotation({{basePackage}}.annotation.OperationLog)")
28
+ public void operationLogPointcut() {
29
+ }
30
+
31
+ @AfterReturning(pointcut = "operationLogPointcut()")
32
+ public void logAfterReturning(JoinPoint joinPoint) {
33
+ OperationLog annotation = annotation(joinPoint);
34
+ log.info(
35
+ "[OperationLog] operator={}, ip={}, action={}, method={}, args={}",
36
+ resolveOperator(),
37
+ resolveIp(),
38
+ annotation.value(),
39
+ joinPoint.getSignature().toShortString(),
40
+ Arrays.toString(joinPoint.getArgs()));
41
+ }
42
+
43
+ @AfterThrowing(pointcut = "operationLogPointcut()", throwing = "error")
44
+ public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
45
+ OperationLog annotation = annotation(joinPoint);
46
+ log.error(
47
+ "[OperationLog] failed operator={}, ip={}, action={}, method={}, error={}",
48
+ resolveOperator(),
49
+ resolveIp(),
50
+ annotation.value(),
51
+ joinPoint.getSignature().toShortString(),
52
+ error.getMessage());
53
+ }
54
+
55
+ private OperationLog annotation(JoinPoint joinPoint) {
56
+ return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(OperationLog.class);
57
+ }
58
+
59
+ private String resolveOperator() {
60
+ try {
61
+ org.springframework.security.core.Authentication authentication =
62
+ org.springframework.security.core.context.SecurityContextHolder.getContext()
63
+ .getAuthentication();
64
+ if (authentication != null
65
+ && authentication.isAuthenticated()
66
+ && !"anonymousUser".equals(authentication.getPrincipal())) {
67
+ return authentication.getName();
68
+ }
69
+ } catch (Throwable ignored) {
70
+ // 沒有 Spring Security 時略過
71
+ }
72
+ ServletRequestAttributes attributes =
73
+ (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
74
+ if (trustForwardedUser && attributes != null) {
75
+ String forwardedUser = attributes.getRequest().getHeader("X-Forwarded-User");
76
+ if (forwardedUser != null && !forwardedUser.isBlank()) {
77
+ return forwardedUser;
78
+ }
79
+ }
80
+ return "anonymous";
81
+ }
82
+
83
+ private String resolveIp() {
84
+ ServletRequestAttributes attributes =
85
+ (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
86
+ if (attributes == null) {
87
+ return "unknown";
88
+ }
89
+ String forwarded = attributes.getRequest().getHeader("X-Forwarded-For");
90
+ if (forwarded != null && !forwarded.isBlank() && !"unknown".equalsIgnoreCase(forwarded)) {
91
+ return forwarded.split(",")[0].trim();
92
+ }
93
+ return attributes.getRequest().getRemoteAddr();
94
+ }
95
+ }
@@ -0,0 +1 @@
1
+ implementation 'org.springframework.boot:spring-boot-starter-aspectj'
@@ -0,0 +1,11 @@
1
+ package {{basePackage}};
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+
6
+ @SpringBootApplication
7
+ public class Application {
8
+ public static void main(String[] args) {
9
+ SpringApplication.run(Application.class, args);
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ package {{basePackage}};
2
+
3
+ import org.junit.jupiter.api.Test;
4
+ import org.springframework.boot.test.context.SpringBootTest;
5
+
6
+ @SpringBootTest
7
+ class ApplicationTests {
8
+
9
+ @Test
10
+ void contextLoads() {}
11
+ }
@@ -0,0 +1,15 @@
1
+ package {{basePackage}}.service;
2
+
3
+ /**
4
+ * 標準 CRUD。XxxService 繼承這個介面;多出來的業務方法寫在 XxxService 上,不要改這裡。
5
+ */
6
+ public interface BaseService<Request, Response, ID> {
7
+
8
+ Response create(Request request);
9
+
10
+ Response getById(ID id);
11
+
12
+ Response update(ID id, Request request);
13
+
14
+ void deleteById(ID id);
15
+ }
@@ -0,0 +1,53 @@
1
+ package {{basePackage}}.service.impl;
2
+
3
+ import java.io.Serializable;
4
+ import org.springframework.transaction.annotation.Transactional;
5
+ import {{daoPackage}}.BaseDAO;
6
+ import {{basePackage}}.exception.NotFoundException;
7
+ import {{basePackage}}.mapper.EntityMapper;
8
+ import {{basePackage}}.service.BaseService;
9
+
10
+ /**
11
+ * CRUD 實作。XxxServiceImpl 把 DAO、Mapper 丟進 super,額外邏輯寫在子類,可直接用 dao、mapper。
12
+ */
13
+ @Transactional
14
+ public abstract class BaseServiceImpl<E, ID extends Serializable, Request, Response>
15
+ implements
16
+ BaseService<Request, Response, ID> {
17
+
18
+ protected final BaseDAO<E, ID> dao;
19
+ protected final EntityMapper<E, Request, Response> mapper;
20
+
21
+ protected BaseServiceImpl(BaseDAO<E, ID> dao, EntityMapper<E, Request, Response> mapper) {
22
+ this.dao = dao;
23
+ this.mapper = mapper;
24
+ }
25
+
26
+ @Override
27
+ public Response create(Request request) {
28
+ return mapper.toResponse(dao.save(mapper.toEntity(request)));
29
+ }
30
+
31
+ @Override
32
+ @Transactional(readOnly = true)
33
+ public Response getById(ID id) {
34
+ return mapper.toResponse(dao.findById(id)
35
+ .orElseThrow(() -> new NotFoundException("查無資料,id:" + id)));
36
+ }
37
+
38
+ @Override
39
+ public Response update(ID id, Request request) {
40
+ E entity = dao.findById(id)
41
+ .orElseThrow(() -> new NotFoundException("找不到資料,無法更新, id = " + id));
42
+ mapper.updateEntity(request, entity);
43
+ return mapper.toResponse(dao.save(entity));
44
+ }
45
+
46
+ @Override
47
+ public void deleteById(ID id) {
48
+ if (!dao.existsById(id)) {
49
+ throw new NotFoundException("找不到資料,無法刪除, id = " + id);
50
+ }
51
+ dao.deleteById(id);
52
+ }
53
+ }
@@ -0,0 +1,12 @@
1
+ package {{basePackage}}.mapper;
2
+
3
+ import org.mapstruct.MappingTarget;
4
+
5
+ public interface EntityMapper<E, Request, Response> {
6
+
7
+ E toEntity(Request request);
8
+
9
+ Response toResponse(E entity);
10
+
11
+ void updateEntity(Request request, @MappingTarget E entity);
12
+ }
@@ -0,0 +1,56 @@
1
+ # {{projectName}}
2
+
3
+ BIRC `create` 產出的 Spring Boot 4 專案。這份說明啟動與生骨架。
4
+ 給 AI coding agent 讀的規範在 `AGENTS.md`(若建立時有注入)。
5
+
6
+ ## 啟動
7
+
8
+ {{#if hasDocker}}
9
+ 先開資料庫,再把 `.env` 載入目前的 shell,最後跑 Spring Boot。
10
+
11
+ Bash / WSL / Git Bash:
12
+
13
+ ```bash
14
+ docker compose up -d db
15
+ set -a && source .env && set +a
16
+ ./gradlew bootRun
17
+ ```
18
+
19
+ PowerShell:
20
+
21
+ ```powershell
22
+ docker compose up -d db
23
+ Get-Content .env | ForEach-Object {
24
+ if ($_ -match '^\s*([^#][^=]*)=(.*)$') {
25
+ Set-Item -Path "Env:$($matches[1].Trim())" -Value $matches[2].Trim().Trim('"')
26
+ }
27
+ }
28
+ .\gradlew.bat bootRun
29
+ ```
30
+ {{else}}
31
+ 先準備 MySQL,並設定 `src/main/resources/application.yml` 的連線,再跑:
32
+
33
+ ```bash
34
+ ./gradlew bootRun
35
+ ```
36
+
37
+ Windows 用 `.\gradlew.bat bootRun`。
38
+ {{/if}}
39
+
40
+ ## 生骨架
41
+
42
+ 在有 `.bircrc.json` 的目錄跑即可;子目錄也可以,`birc` 會往上找。
43
+
44
+ ```bash
45
+ birc make
46
+ birc make:entity User --example --fields title:String
47
+ birc add email
48
+ ```
49
+
50
+ ## 不要刪的標記
51
+
52
+ `birc add` 靠這幾行插入設定。刪了會預檢查失敗、不寫任何檔。
53
+
54
+ - `src/main/resources/application.yml`:`# birc-generator:config-anchor`
55
+ - `build.gradle`:`// birc-generator:dependency-anchor`
56
+ - 若有:`// birc-generator:plugin-anchor`、`// birc-generator:allprojects-anchor`
@@ -0,0 +1,48 @@
1
+ package {{basePackage}}.web;
2
+
3
+ /**
4
+ * API 統一外殼。成功帶 data,失敗帶 errorCode / message。
5
+ * 業務錯誤仍丟 ProjectException,由 ExceptionHandleController 轉成 failure。
6
+ */
7
+ public class Result<T> {
8
+
9
+ private final boolean success;
10
+ private final T data;
11
+ private final String errorCode;
12
+ private final String message;
13
+
14
+ private Result(boolean success, T data, String errorCode, String message) {
15
+ this.success = success;
16
+ this.data = data;
17
+ this.errorCode = errorCode;
18
+ this.message = message;
19
+ }
20
+
21
+ public static <T> Result<T> success(T data) {
22
+ return new Result<>(true, data, null, null);
23
+ }
24
+
25
+ public static Result<Void> success() {
26
+ return new Result<>(true, null, null, null);
27
+ }
28
+
29
+ public static <T> Result<T> failure(String errorCode, String message) {
30
+ return new Result<>(false, null, errorCode, message);
31
+ }
32
+
33
+ public boolean isSuccess() {
34
+ return success;
35
+ }
36
+
37
+ public T getData() {
38
+ return data;
39
+ }
40
+
41
+ public String getErrorCode() {
42
+ return errorCode;
43
+ }
44
+
45
+ public String getMessage() {
46
+ return message;
47
+ }
48
+ }
@@ -0,0 +1,27 @@
1
+ spring:
2
+ datasource:
3
+ url: jdbc:h2:mem:{{snakeCase projectNameKebab}};MODE=MySQL;DB_CLOSE_DELAY=-1
4
+ driver-class-name: org.h2.Driver
5
+ username: sa
6
+ password:
7
+ jpa:
8
+ hibernate:
9
+ ddl-auto: none
10
+ flyway:
11
+ enabled: false
12
+ mail:
13
+ host: localhost
14
+ port: 3025
15
+ username: test
16
+ password: test
17
+
18
+ sentry:
19
+ dsn:
20
+
21
+ birc:
22
+ sso:
23
+ enabled: false
24
+
25
+ external:
26
+ api:
27
+ base-url: https://example.com