@deepstorm/cli 0.1.0

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 (148) hide show
  1. package/README.md +72 -0
  2. package/dist/agents/reef-inspect-figma.md +77 -0
  3. package/dist/agents/reef-review-backend.md.tmpl +112 -0
  4. package/dist/agents/reef-review-frontend.md.tmpl +78 -0
  5. package/dist/agents/reef-review-infra.md +47 -0
  6. package/dist/agents/reef-review-security.md.tmpl +80 -0
  7. package/dist/agents/reef-scope-analysis.md +64 -0
  8. package/dist/build-registry.js +375 -0
  9. package/dist/cli.js +8581 -0
  10. package/dist/config-schema.json +133 -0
  11. package/dist/env-examples/context7.env-example +19 -0
  12. package/dist/env-examples/feishu-wiki.env-example +16 -0
  13. package/dist/env-examples/figma.env-example +16 -0
  14. package/dist/env-examples/github.env-example +20 -0
  15. package/dist/env-examples/jira.env-example +20 -0
  16. package/dist/hooks/mcp-hook.sh +77 -0
  17. package/dist/hooks/reef-auto-format.sh.tmpl +72 -0
  18. package/dist/hooks/reef-block-dangerous.sh +70 -0
  19. package/dist/hooks/reef-hooks.json +72 -0
  20. package/dist/hooks/reef-intent-detect.sh +129 -0
  21. package/dist/hooks/reef-protect-files.sh +55 -0
  22. package/dist/hooks/reef-run-tests.sh +84 -0
  23. package/dist/hooks/reef-scope-check.sh +386 -0
  24. package/dist/hooks/reef-scope-ci.sh +28 -0
  25. package/dist/hooks/reef-scope-gate.sh +115 -0
  26. package/dist/hooks/reef-scope-pre-commit.sh.tmpl +28 -0
  27. package/dist/hooks/reef-scope-setup.sh +204 -0
  28. package/dist/hooks/reef-scope-split.sh +203 -0
  29. package/dist/hooks/sweep-hooks.json +14 -0
  30. package/dist/hooks/sweep-mcp-hook.sh +77 -0
  31. package/dist/hooks/tide-hooks.json +14 -0
  32. package/dist/hooks/tide-session-preload.sh +17 -0
  33. package/dist/mcp/code-hosting/github.json +20 -0
  34. package/dist/mcp/design-tools/figma.json +19 -0
  35. package/dist/mcp/docs-reference/context7.json +28 -0
  36. package/dist/mcp/e2e-testing/playwright.json +13 -0
  37. package/dist/mcp/knowledge-base/feishu-wiki.json +19 -0
  38. package/dist/mcp/project-management/jira.json +27 -0
  39. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-read/SKILL.md +65 -0
  40. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-write/SKILL.md +63 -0
  41. package/dist/mcp-skills/deepflow-mcp-figma-read/SKILL.md +98 -0
  42. package/dist/mcp-skills/deepflow-mcp-github-read/SKILL.md +62 -0
  43. package/dist/mcp-skills/deepflow-mcp-github-write/SKILL.md +63 -0
  44. package/dist/mcp-skills/deepflow-mcp-jira-read/SKILL.md +80 -0
  45. package/dist/mcp-skills/deepflow-mcp-jira-write/SKILL.md +74 -0
  46. package/dist/mcp-skills/deepflow-mcp-playwright-read/SKILL.md +79 -0
  47. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +65 -0
  48. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +63 -0
  49. package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +98 -0
  50. package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +62 -0
  51. package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +63 -0
  52. package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +80 -0
  53. package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +74 -0
  54. package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +79 -0
  55. package/dist/registry.json +818 -0
  56. package/dist/skills/atoll-ops/SKILL.md +46 -0
  57. package/dist/skills/reef-commit/SKILL.md +127 -0
  58. package/dist/skills/reef-gen-backend/SKILL.md.tmpl +87 -0
  59. package/dist/skills/reef-gen-backend/variants/java/steps.md +28 -0
  60. package/dist/skills/reef-gen-backend/variants/python/steps.md +70 -0
  61. package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +83 -0
  62. package/dist/skills/reef-gen-frontend/variants/angular/steps.md +30 -0
  63. package/dist/skills/reef-harden/EXAMPLES.md +89 -0
  64. package/dist/skills/reef-harden/SKILL.md +136 -0
  65. package/dist/skills/reef-pr/SKILL.md +97 -0
  66. package/dist/skills/reef-review/SKILL.md.tmpl +107 -0
  67. package/dist/skills/reef-scope/SKILL.md +134 -0
  68. package/dist/skills/reef-start/SKILL.md.tmpl +562 -0
  69. package/dist/skills/reef-start/references/jira-start-subagent.md +60 -0
  70. package/dist/skills/reef-style-backend/SKILL.md.tmpl +134 -0
  71. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/chat-client.md +96 -0
  72. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/rag.md +94 -0
  73. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +62 -0
  74. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/tool-calling.md +68 -0
  75. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +220 -0
  76. package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +148 -0
  77. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +131 -0
  78. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/quick-reference.md +103 -0
  79. package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +119 -0
  80. package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +101 -0
  81. package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +181 -0
  82. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/controller.md +95 -0
  83. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +121 -0
  84. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/infrastructure.md +179 -0
  85. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +202 -0
  86. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +107 -0
  87. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +83 -0
  88. package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +150 -0
  89. package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +197 -0
  90. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/examples/user-repository-test.md +69 -0
  91. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +101 -0
  92. package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md +61 -0
  93. package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +100 -0
  94. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/examples/user-controller-test.md +61 -0
  95. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +85 -0
  96. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/examples/user-service-integration-test.md +56 -0
  97. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +83 -0
  98. package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +77 -0
  99. package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +164 -0
  100. package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +139 -0
  101. package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +177 -0
  102. package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +101 -0
  103. package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +135 -0
  104. package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +111 -0
  105. package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +83 -0
  106. package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +207 -0
  107. package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +91 -0
  108. package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +227 -0
  109. package/dist/skills/reef-style-backend/variants/java/examples/contributor-pattern.md +97 -0
  110. package/dist/skills/reef-style-backend/variants/java/quick-reference.md +117 -0
  111. package/dist/skills/reef-style-backend/variants/python/examples/crud-router.md +71 -0
  112. package/dist/skills/reef-style-backend/variants/python/examples/pydantic-schema.md +45 -0
  113. package/dist/skills/reef-style-backend/variants/python/examples/pytest-fixture.md +60 -0
  114. package/dist/skills/reef-style-backend/variants/python/examples/sqlalchemy-model.md +31 -0
  115. package/dist/skills/reef-style-backend/variants/python/quick-reference.md +111 -0
  116. package/dist/skills/reef-style-frontend/SKILL.md.tmpl +70 -0
  117. package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +79 -0
  118. package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +150 -0
  119. package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +141 -0
  120. package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +112 -0
  121. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +134 -0
  122. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/quick-reference.md +66 -0
  123. package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +252 -0
  124. package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +56 -0
  125. package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +100 -0
  126. package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +119 -0
  127. package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +95 -0
  128. package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +100 -0
  129. package/dist/skills/reef-testcase/SKILL.md +138 -0
  130. package/dist/skills/reef-testcase/references/coverage-dimensions.md +99 -0
  131. package/dist/skills/reef-testcase/references/test-case-template.md +63 -0
  132. package/dist/skills/sweep-init/SKILL.md +286 -0
  133. package/dist/skills/sweep-init/scripts/flow-selector.mjs +305 -0
  134. package/dist/skills/sweep-plan/SKILL.md.tmpl +312 -0
  135. package/dist/skills/sweep-plan/references/test-flow-template.md +46 -0
  136. package/dist/skills/sweep-run/SKILL.md +437 -0
  137. package/dist/skills/sweep-run/scripts/env-manager.mjs +208 -0
  138. package/dist/skills/sweep-run/scripts/flow-parser.mjs +329 -0
  139. package/dist/skills/sweep-run/scripts/flow-selector.mjs +483 -0
  140. package/dist/skills/sweep-run/scripts/mcp-manager.mjs +208 -0
  141. package/dist/skills/sweep-run/scripts/spec-compiler.mjs +303 -0
  142. package/dist/skills/tide-discuss/SKILL.md.tmpl +449 -0
  143. package/dist/skills/tide-discuss/references/checklists.md +88 -0
  144. package/dist/skills/tide-discuss/references/data-format.md +237 -0
  145. package/dist/skills/tide-discuss/references/prd-template.md +134 -0
  146. package/dist/skills/tide-discuss/references/publish-flow.md +167 -0
  147. package/dist/skills/tide-discuss/references/role-prompts.md +105 -0
  148. package/package.json +38 -0
@@ -0,0 +1,202 @@
1
+ # 后端 Service/Entity/Repository 示例
2
+
3
+ ## 1. Service(getEntity 模式 + @Transactional)
4
+
5
+ ```java
6
+ @Service
7
+ @AllArgsConstructor
8
+ public class FormService {
9
+ private final FormRepository formRepository;
10
+ private final ApplicationService appService;
11
+
12
+ public FormDetailsDto getForm(Long appId, Long formId) {
13
+ return FormMapper.INSTANCE.mapToDetails(getFormEntity(appId, formId));
14
+ }
15
+
16
+ public Form getFormEntity(Long appId, Long formId) {
17
+ return formRepository.findByIdAndAppId(formId, appId).orElseThrow(
18
+ () -> new NotFoundException("表单不存在", new ResourceInfo("Form", formId)));
19
+ }
20
+
21
+ @Transactional
22
+ public FormDetailsDto createForm(Long appId, CreateFormRequest request) {
23
+ var app = appService.getApplicationEntity(appId);
24
+ var form = new Form("新的表单", request.approvalEnabled(), app.getId());
25
+ return FormMapper.INSTANCE.mapToDetails(formRepository.save(form));
26
+ }
27
+
28
+ @Transactional
29
+ public void deleteForm(Long appId, Long formId) {
30
+ formRepository.delete(getFormEntity(appId, formId));
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## 2. Repository(多租户感知)
36
+
37
+ ```java
38
+ public interface FormRepository extends JpaRepository<Form, Long> {
39
+ List<Form> findAllByAppId(Long appId);
40
+ Optional<Form> findByIdAndAppId(Long id, Long appId);
41
+
42
+ @Query("""
43
+ select distinct f from Form f
44
+ left join fetch f.revisions r
45
+ left join fetch r.items
46
+ where f.id = ?1 and f.appId = ?2
47
+ """)
48
+ Optional<Form> findByIdAndAppIdWithRevisionsAndItems(
49
+ @Param("id") Long id, @Param("appId") Long appId);
50
+ }
51
+ ```
52
+
53
+ ## 3. 实体
54
+
55
+ | 场景 | 基类 |
56
+ |------|------|
57
+ | 普通多租户,可变 | `AbstractTenantAwareEntity` |
58
+ | 多租户 + 审计字段 | `AbstractTenantAwareAuditable` |
59
+ | 多租户,不可变 + 审计 | `AbstractTenantAwareImmutableAuditable` |
60
+ | 非租户实体(极少) | `AbstractEntity` 或 `AbstractAuditable` |
61
+
62
+ ```java
63
+ @Entity
64
+ @Table(name = "app_form", indexes = { @Index(columnList = "app_id") })
65
+ @NoArgsConstructor(access = AccessLevel.PROTECTED)
66
+ public class Form extends AbstractTenantAwareEntity {
67
+ @NotBlank @Column(nullable = false)
68
+ private String title;
69
+ @Column(name = "approval_enabled", nullable = false)
70
+ private boolean approvalEnabled;
71
+ @Column(name = "app_id", nullable = false)
72
+ private Long appId;
73
+ @OneToMany(mappedBy = "form", cascade = CascadeType.ALL, orphanRemoval = true)
74
+ private List<FormRevision> revisions = new ArrayList<>();
75
+
76
+ @Default
77
+ public Form(Long id) { super(id); }
78
+
79
+ public Form(String title, boolean approvalEnabled, Long appId) {
80
+ this.title = title; this.approvalEnabled = approvalEnabled; this.appId = appId;
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## 4. 事件驱动级联删除
86
+
87
+ 跨 Service 的关联数据清理通过事件驱动解耦。三个原则:谁删除谁发布事件,谁清理谁监听事件,事件参数最小化。
88
+
89
+ ```mermaid
90
+ flowchart LR
91
+ DELETE["FormService.deleteForm()"] --> REPO["formRepository.delete(form)"]
92
+ DELETE --> EVENT["eventPublisher.publishEvent(event)"]
93
+ EVENT --> FRS["FormResponseService @EventListener<br/>删除 FormResponse"]
94
+ EVENT --> LES["LogEntryService @EventListener<br/>删除相关日志"]
95
+ ```
96
+
97
+ ```java
98
+ // 发布方
99
+ @Service @AllArgsConstructor
100
+ public class FormService {
101
+ private final ApplicationEventPublisher eventPublisher;
102
+
103
+ @Transactional
104
+ public void deleteForm(Long appId, Long formId) {
105
+ var form = getFormEntity(appId, formId);
106
+ formRepository.delete(form);
107
+ eventPublisher.publishEvent(new FormDeletedEvent(appId, formId, form.getTitle()));
108
+ }
109
+ }
110
+
111
+ // 事件类(@Getter @AllArgsConstructor,private final 不可变)
112
+ @Getter @AllArgsConstructor
113
+ public class FormDeletedEvent {
114
+ private final Long appId;
115
+ private final Long formId;
116
+ private final String formTitle;
117
+ }
118
+
119
+ // 监听方(各自处理自己的数据范围)
120
+ @Service
121
+ public class FormResponseService {
122
+ @EventListener @Transactional
123
+ public void handleFormDeletedEvent(FormDeletedEvent event) {
124
+ formResponseRepository.deleteByFormId(event.getFormId());
125
+ }
126
+ }
127
+ ```
128
+
129
+ 事件发布注意事项:事件参数顺序 `appId` → 父级 ID → 自身 ID → name;监听方法加 `@Transactional`;事务提交后事件才派发。
130
+
131
+ ## 5. Reference Contributor 模式
132
+
133
+ 删除资源前查询哪些其他资源引用了它。完整代码示例见 [`contributor-pattern.md`](contributor-pattern.md)。
134
+
135
+ ## 6. 日志实体继承
136
+
137
+ 所有日志使用 `SINGLE_TABLE` 策略,统一存储在 `log_entry` 表:
138
+
139
+ ```mermaid
140
+ classDiagram
141
+ class LogEntry { +appId }
142
+ class ActionLogEntry { +title +record +actionTitle +时间 +status }
143
+ class FormActionLogEntry { +formResponseId }
144
+ class TableActionLogEntry
145
+ class FormDeletionLogEntry { +formId +formTitle }
146
+ LogEntry <|-- ActionLogEntry
147
+ ActionLogEntry <|-- FormActionLogEntry
148
+ ActionLogEntry <|-- TableActionLogEntry
149
+ LogEntry <|-- FormDeletionLogEntry
150
+ ```
151
+
152
+ ```java
153
+ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter
154
+ public class FormDeletionLogEntry extends LogEntry {
155
+ private Long formId;
156
+ private String formTitle;
157
+
158
+ public FormDeletionLogEntry(Long appId, Long formId, String formTitle) {
159
+ super(appId);
160
+ this.formId = formId; this.formTitle = formTitle;
161
+ }
162
+ }
163
+ ```
164
+
165
+ 关键规则:新日志类型继承 `LogEntry`,不直接继承 `AbstractTenantAwareImmutableAuditable`;构造函数调 `super(appId)`;`createdById` 由 `AuditingEntityListener` 自动填充;新增字段在 Liquibase 中 `nullable = true`。
166
+
167
+ ## 7. 长时运行异步操作(AIP-151)
168
+
169
+ 耗时操作(如 CSV 导入)通过 `Operation` 抽象实体 + `@DomainEvents` 模式实现异步状态管理:
170
+
171
+ ```mermaid
172
+ classDiagram
173
+ class Operation { <<abstract>> +Status +Metadata metadata +Result result +run() +doRun() }
174
+ class Metadata { <<abstract>> }
175
+ class Result { <<abstract>> }
176
+ class EmptyResult
177
+ class ErrorResult
178
+ Operation --> Metadata : @Embedded
179
+ Operation --> Result : @Embedded
180
+ Result <|-- EmptyResult
181
+ Result <|-- ErrorResult
182
+ ```
183
+
184
+ ```java
185
+ @Service @AllArgsConstructor
186
+ public class DictionaryItemService {
187
+ private final Validator validator;
188
+
189
+ @Transactional
190
+ public OperationDto importDictionaryItems(Long appId, Long dictId, MultipartFile file) {
191
+ var items = parseCsvFile(file);
192
+ List<String> messages = validateItems(items);
193
+ if (!messages.isEmpty()) {
194
+ return new ImportDictionaryItemsOperationDto(/* ... */);
195
+ }
196
+ repository.saveAll(items);
197
+ return new ImportDictionaryItemsOperationDto(/* ... */);
198
+ }
199
+ }
200
+ ```
201
+
202
+ 关键规则:校验失败返回 `Status.FAILED` + 错误信息,不抛异常;`@DomainEvents` 在实体保存后自动派发;`@Embedded Metadata` / `Result` 支持子类型多态。DTO 层次使用 `@JsonTypeInfo` + `@JsonSubTypes` 实现多态 JSON 序列化。
@@ -0,0 +1,107 @@
1
+ # 后端测试示例
2
+
3
+ ---
4
+
5
+ ## 1. Controller 测试
6
+
7
+ ```java
8
+ @WebMvcTest(FormController.class)
9
+ @Import({SecurityConfig.class, TestUserConfig.class})
10
+ @WithUserDetails(
11
+ value = "admin", userDetailsServiceBeanName = "testUserDetailsService")
12
+ @ActiveProfiles("test")
13
+ class FormControllerTest {
14
+ @Autowired private MockMvc mvc;
15
+ @Autowired private ObjectMapper objectMapper;
16
+ @MockitoBean private FormService service;
17
+
18
+ @Test
19
+ void shouldReturnNotFound_whenFormDoesNotExist() throws Exception {
20
+ when(service.getForm(1L, 2L))
21
+ .thenThrow(new NotFoundException("表单不存在", new ResourceInfo("Form", 2L)));
22
+ mvc.perform(get("/api/v1/apps/1/forms/2"))
23
+ .andExpect(status().isNotFound())
24
+ .andExpect(jsonPath("$.code").value(404))
25
+ .andExpect(jsonPath("$.message").value("表单不存在"));
26
+ }
27
+
28
+ @Test
29
+ void shouldPublishForm() throws Exception {
30
+ var request = new PublishFormRequest(List.of(control), List.of());
31
+ when(service.publishForm(
32
+ eq(1L), eq(2L), any(PublishFormRequest.class)))
33
+ .thenReturn(FormMapper.INSTANCE.mapToDetails(form));
34
+ mvc.perform(post("/api/v1/apps/1/forms/2:publish")
35
+ .contentType(MediaType.APPLICATION_JSON)
36
+ .content(objectMapper.writeValueAsString(request)))
37
+ .andExpect(status().isOk())
38
+ .andExpect(jsonPath("$.title").value("form1"));
39
+ }
40
+ }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 2. 多 Controller 测试
46
+
47
+ ```java
48
+ @WebMvcTest({ FormController.class, FormRevisionController.class })
49
+ @Import({SecurityConfig.class, TestUserConfig.class})
50
+ @WithUserDetails(
51
+ value = "admin", userDetailsServiceBeanName = "testUserDetailsService")
52
+ @ActiveProfiles("test")
53
+ class FormRevisionControllerTest {
54
+ @Autowired private MockMvc mvc;
55
+ @MockitoBean private FormService formService;
56
+ @MockitoBean private FormRevisionService formRevisionService;
57
+ }
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 3. Service 集成测试
63
+
64
+ ```java
65
+ @SpringBootTest
66
+ @Transactional
67
+ @ActiveProfiles("test")
68
+ class ApplicationServiceTest {
69
+ @Autowired private ApplicationService service;
70
+ @Autowired private ApplicationRepository repository;
71
+
72
+ @Test
73
+ void shouldCreateApplication() {
74
+ var request = new CreateApplicationRequest("app1", null, null);
75
+ var dto = service.createApplication(request);
76
+ assertThat(dto.getName()).isEqualTo("app1");
77
+ assertThat(dto.getId()).isNotNull();
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 4. Service 事件测试
85
+
86
+ ```java
87
+ @SpringBootTest
88
+ @Transactional
89
+ @ActiveProfiles("test")
90
+ class FormServiceTest {
91
+
92
+ @Autowired private FormService formService;
93
+ @MockitoBean private FormRepository formRepository;
94
+ @Autowired private ApplicationEventPublisher eventPublisher;
95
+
96
+ @Test
97
+ void should_publish_event_when_delete_form() {
98
+ var captor = ArgumentCaptor.forClass(FormDeletedEvent.class);
99
+ verify(eventPublisher).publishEvent(captor.capture());
100
+ assertThat(captor.getValue().formId()).isEqualTo(1L);
101
+ }
102
+ }
103
+ ```
104
+
105
+ 关键点:
106
+ - `ArgumentCaptor` 验证事件发布,不直接 Mock `ApplicationEventPublisher`
107
+ - `@Transactional` 确保每个测试回滚,互不干扰
@@ -0,0 +1,83 @@
1
+ # Spring Boot 规范
2
+
3
+ 按需加载。仅当需要编写对应组件类型时阅读相关章节。
4
+
5
+ > **完整示例代码见 `examples/` 目录**(controller.md、service-entity.md、dto-mapper.md、testing.md、infrastructure.md)。
6
+
7
+ ## 速查
8
+
9
+ | 场景 | 决策 |
10
+ | --- | --- |
11
+ | 新建 Controller | `@RestController` + `@RequestMapping("/api/v1/apps/{appId}/...")` + `@RequiredArgsConstructor` |
12
+ | 新建 Service | `@Service` + `@Transactional` + `@RequiredArgsConstructor`,字段 `private final` |
13
+ | 新建 Repository | 继承 `JpaRepository<Entity, Long>` |
14
+ | 新建立方 DTO | 继承 `AbstractDto` 等基类或使用 `@Value` |
15
+ | 新建写请求 | 独立 `record CreateRequest` / `UpdateRequest` |
16
+ | 对象映射 | MapStruct `@Mapper(config = MapStructConfig.class)` |
17
+ | 异常处理 | 使用项目自定义异常(`NotFoundException` 等) |
18
+ | 多租户查询 | 禁止裸 `findById`,使用 `findByIdAndAppId`;禁止 `createNativeQuery` |
19
+ | Controller 权限 | 写操作接口加 `@PreAuthorize("hasAuthority('...')")` |
20
+ | 密码存储 | `BCryptPasswordEncoder` |
21
+ | 弃用 API | 编译警告中的 `@Deprecated` API 在同一次 PR 中替换为新 API |
22
+
23
+ ## 核心规范
24
+
25
+ ### Controller 层
26
+
27
+ - `@RestController` + `@RequestMapping("/api/v1/apps/{appId}/...")` + `@AllArgsConstructor`
28
+ - 标准 CRUD:list / get / create / update / delete
29
+ - 自定义方法用 AIP-136 冒号语法:`@PostMapping("/{id}:publish")`
30
+ - 写操作 DTO 加 `@Valid`,嵌套对象加 `@Valid`
31
+ - API 版本策略:非破坏性变更停留在当前版本;破坏性变更创建新 API 版本
32
+
33
+ ### Service 层
34
+
35
+ - `@Service` + `@AllArgsConstructor`,依赖 `private final`
36
+ - 提取 `getEntity()` 复用查找 + 异常抛出
37
+ - 写操作加 `@Transactional`
38
+ - 多租户查询:`repository.findByIdAndAppId(id, appId)` — 禁止裸 `findById`
39
+ - 级联清理通过事件驱动(`eventPublisher.publishEvent(event)`)
40
+
41
+ ### Repository 层
42
+
43
+ 继承 `JpaRepository<Entity, Long>`。多租户实体提供 `findAllByAppId` 和 `findByIdAndAppId`。`@TenantId` 由 Hibernate 自动过滤,禁止 `createNativeQuery`。
44
+
45
+ ### DTO / MapStruct
46
+
47
+ DTO 继承基类,写请求用独立 CreateRequest/UpdateRequest record。MapStruct:
48
+
49
+ - `@Mapper(config = MapStructConfig.class, uses = { ... })`
50
+ - 声明 `INSTANCE = Mappers.getMapper(...)`
51
+ - 子映射通过 `uses` 组合
52
+ - 映射方法命名区分:`toSummary()` / `toDetail()`
53
+
54
+ ### 异常处理
55
+
56
+ | 异常 | HTTP | 场景 |
57
+ | --- | --- | --- |
58
+ | `NotFoundException` | 404 | 资源不存在 |
59
+ | `AlreadyExistsException` | 409 | 重复创建 |
60
+ | `InvalidArgumentException` | 400 | 参数校验失败 |
61
+ | `PermissionDeniedException` | 403 | 无权限 |
62
+ | `FailedPreconditionException` | 400 | 前置条件不满足 |
63
+
64
+ 所有自定义异常通过 `@RestControllerAdvice` `GlobalExceptionHandler` 统一处理,返回 AIP-193 兼容的 `CustomError` 结构:
65
+
66
+ ```java
67
+ @RestControllerAdvice
68
+ public class GlobalExceptionHandler {
69
+ @ExceptionHandler(ResourceNotFoundException.class)
70
+ public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
71
+ return ResponseEntity.status(NOT_FOUND).body(new ErrorResponse(ex.getMessage()));
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### 安全编码规范
77
+
78
+ - Controller 写操作接口必须加 `@PreAuthorize("hasAuthority('PERMISSION_NAME')")`,禁止在方法内硬编码权限字符串
79
+ - 请求体参数加 `@Valid`,嵌套对象加 `@Valid`
80
+ - DTO 中密码/token 用 `@JsonIgnore` 或从响应 DTO 中排除
81
+ - 密码使用 `BCryptPasswordEncoder`
82
+ - 禁止 `createNativeQuery`(绕过多租户 + SQL 注入风险)
83
+ - `@Query` 始终使用命名参数(`:paramName`),不用 `?1` 位置参数
@@ -0,0 +1,150 @@
1
+ # Hibernate / JPA 规范
2
+
3
+ 按需加载。仅当需要编写对应组件类型时阅读相关章节。
4
+
5
+ ## 速查
6
+
7
+ | 场景 | 决策 |
8
+ | --- | --- |
9
+ | 新建实体 | 继承 `AbstractTenantAwareEntity` 或 `AbstractTenantAwareAuditable` |
10
+ | 实体 `@Table` 命名 | 小写蛇形复数,如 `app_forms` |
11
+ | 审计字段 | `createdById` 由 `AuditingEntityListener` 自动填充 |
12
+ | 日志实体 | 继承 `LogEntry`(SINGLE_TABLE),不直接继承基类 |
13
+ | `open-in-view` | 设为 `false` |
14
+
15
+ ## 概述
16
+
17
+ 使用 Hibernate 作为 JPA 实现进行对象关系映射。
18
+
19
+ ## Entity 规范
20
+
21
+ ```java
22
+ @Entity
23
+ @Table(name = "users")
24
+ @NoArgsConstructor(access = PROTECTED)
25
+ @Getter
26
+ @SuperBuilder
27
+ public class User extends AbstractAuditable {
28
+
29
+ @Id
30
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
31
+ private Long id;
32
+
33
+ @Column(nullable = false, length = 100)
34
+ private String name;
35
+
36
+ @Column(unique = true, nullable = false)
37
+ private String email;
38
+
39
+ @ManyToOne(fetch = LAZY)
40
+ @JoinColumn(name = "department_id")
41
+ private Department department;
42
+
43
+ @OneToMany(mappedBy = "user", cascade = ALL, orphanRemoval = true)
44
+ private List<Order> orders = new ArrayList<>();
45
+ }
46
+ ```
47
+
48
+ **规则:**
49
+ - 继承正确的基类(参见「实体/DTO 层次」章节)
50
+ - 三件套:`@Entity` + `@NoArgsConstructor(access = PROTECTED)`(final 字段加 `force = true`),`@Getter` 类级别或字段级别,`@SuperBuilder` 按需使用
51
+ - `@Table(name = "...")` 命名:小写蛇形,复数
52
+ - 字段用 `@Column(nullable = false)` 等约束
53
+ - 关联关系:多租户实体间的关联用 `@ManyToOne(fetch = LAZY)` + `@JoinColumn(name = "...")`,避免 EAGER
54
+ - 日志实体继承 `LogEntry`(SINGLE_TABLE),不要直接继承 `AbstractTenantAwareImmutableAuditable`
55
+ - `createdById` 由 `AuditingEntityListener` 自动填充,构造函数不需要传递
56
+
57
+ ## 实体/DTO 层次
58
+
59
+ ```mermaid
60
+ classDiagram
61
+ class AbstractImmutable {
62
+ +id
63
+ +createdAt
64
+ }
65
+ class AbstractEntity {
66
+ +lastModifiedAt
67
+ +version @Version
68
+ }
69
+ class AbstractTenantAwareEntity {
70
+ +tenantId @TenantId
71
+ }
72
+ class AbstractTenantAwareAuditable {
73
+ +createdBy
74
+ +lastModifiedBy
75
+ }
76
+ class AbstractImmutableAuditable {
77
+ +createdBy
78
+ }
79
+ class AbstractTenantAwareImmutableAuditable {
80
+ +tenantId @TenantId
81
+ +lastModifiedBy
82
+ }
83
+
84
+ AbstractImmutable <|-- AbstractEntity
85
+ AbstractEntity <|-- AbstractTenantAwareEntity
86
+ AbstractTenantAwareEntity <|-- AbstractTenantAwareAuditable
87
+ AbstractImmutable <|-- AbstractImmutableAuditable
88
+ AbstractImmutableAuditable <|-- AbstractTenantAwareImmutableAuditable
89
+ ```
90
+
91
+ - 所有实体使用 Lombok(`@Getter`、`@NoArgsConstructor`、`@SuperBuilder` 按需使用)和 `@MappedSuperclass`。
92
+ - 每个实体有独立的数据库序列,`incrementBy=50`(匹配 Hibernate 默认序列优化)。
93
+ - 审计字段由 `AuditingConfig` 通过 Spring Security `SecurityContextHolder` 自动填充。
94
+
95
+ **后端 DTO**
96
+ `AbstractImmutableDto` / `AbstractDto` / `AbstractImmutableAuditableDto` / `AbstractAuditableDto` 对应实体层次。`PagedResponse<T>` 统一分页响应(`items` + `totalItems`)。
97
+
98
+ > 完整集成示例(含 Service + Entity + Repository)见框架维度的 `examples/service-entity.md`。
99
+
100
+ ## 关系映射
101
+
102
+ | 关系 | 注解 | Fetch 策略 | 使用场景 |
103
+ |------|------|-----------|---------|
104
+ | 多对一 | `@ManyToOne` | LAZY (默认) | 子→父引用 |
105
+ | 一对多 | `@OneToMany` | LAZY (默认) | 父→子集合 |
106
+ | 一对一 | `@OneToOne` | LAZY (显式) | 用户→档案 |
107
+ | 多对多 | `@ManyToMany` | LAZY (显式) | 用户→角色 |
108
+
109
+ ## 查询
110
+
111
+ ### JPQL / Criteria
112
+
113
+ ```java
114
+ @Repository
115
+ public interface UserRepository extends JpaRepository<User, Long> {
116
+
117
+ // 方法命名查询
118
+ List<User> findByDepartmentId(Long departmentId);
119
+
120
+ // JPQL
121
+ @Query("SELECT u FROM User u WHERE u.email LIKE :domain")
122
+ List<User> findByEmailDomain(@Param("domain") String domain);
123
+
124
+ // 更新操作
125
+ @Modifying
126
+ @Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
127
+ int deactivateInactiveUsers(@Param("date") LocalDateTime date);
128
+ }
129
+ ```
130
+
131
+ ### EntityGraph (N+1 问题)
132
+
133
+ ```java
134
+ @Entity
135
+ @NamedEntityGraph(name = "User.orders", attributeNodes = @NamedAttributeNode("orders"))
136
+ public class User { ... }
137
+
138
+ // Repository
139
+ @Query("SELECT u FROM User u WHERE u.id = :id")
140
+ @EntityGraph("User.orders")
141
+ Optional<User> findByIdWithOrders(@Param("id") Long id);
142
+ ```
143
+
144
+ ## 最佳实践
145
+
146
+ - ✅ `open-in-view: false`(避免懒加载异常掩盖性能问题)
147
+ - ✅ `ddl-auto: validate`(生产环境不做自动 DDL)
148
+ - ✅ 使用 `@DynamicUpdate` 优化更新语句
149
+ - ❌ 避免 EAGER fetch(一律使用 LAZY + EntityGraph)
150
+ - ❌ 避免在循环中查询数据库(批量使用 `findAllById` / `IN` 查询)