@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.
- package/README.md +72 -0
- package/dist/agents/reef-inspect-figma.md +77 -0
- package/dist/agents/reef-review-backend.md.tmpl +112 -0
- package/dist/agents/reef-review-frontend.md.tmpl +78 -0
- package/dist/agents/reef-review-infra.md +47 -0
- package/dist/agents/reef-review-security.md.tmpl +80 -0
- package/dist/agents/reef-scope-analysis.md +64 -0
- package/dist/build-registry.js +375 -0
- package/dist/cli.js +8581 -0
- package/dist/config-schema.json +133 -0
- package/dist/env-examples/context7.env-example +19 -0
- package/dist/env-examples/feishu-wiki.env-example +16 -0
- package/dist/env-examples/figma.env-example +16 -0
- package/dist/env-examples/github.env-example +20 -0
- package/dist/env-examples/jira.env-example +20 -0
- package/dist/hooks/mcp-hook.sh +77 -0
- package/dist/hooks/reef-auto-format.sh.tmpl +72 -0
- package/dist/hooks/reef-block-dangerous.sh +70 -0
- package/dist/hooks/reef-hooks.json +72 -0
- package/dist/hooks/reef-intent-detect.sh +129 -0
- package/dist/hooks/reef-protect-files.sh +55 -0
- package/dist/hooks/reef-run-tests.sh +84 -0
- package/dist/hooks/reef-scope-check.sh +386 -0
- package/dist/hooks/reef-scope-ci.sh +28 -0
- package/dist/hooks/reef-scope-gate.sh +115 -0
- package/dist/hooks/reef-scope-pre-commit.sh.tmpl +28 -0
- package/dist/hooks/reef-scope-setup.sh +204 -0
- package/dist/hooks/reef-scope-split.sh +203 -0
- package/dist/hooks/sweep-hooks.json +14 -0
- package/dist/hooks/sweep-mcp-hook.sh +77 -0
- package/dist/hooks/tide-hooks.json +14 -0
- package/dist/hooks/tide-session-preload.sh +17 -0
- package/dist/mcp/code-hosting/github.json +20 -0
- package/dist/mcp/design-tools/figma.json +19 -0
- package/dist/mcp/docs-reference/context7.json +28 -0
- package/dist/mcp/e2e-testing/playwright.json +13 -0
- package/dist/mcp/knowledge-base/feishu-wiki.json +19 -0
- package/dist/mcp/project-management/jira.json +27 -0
- package/dist/mcp-skills/deepflow-mcp-feishu-wiki-read/SKILL.md +65 -0
- package/dist/mcp-skills/deepflow-mcp-feishu-wiki-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepflow-mcp-figma-read/SKILL.md +98 -0
- package/dist/mcp-skills/deepflow-mcp-github-read/SKILL.md +62 -0
- package/dist/mcp-skills/deepflow-mcp-github-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepflow-mcp-jira-read/SKILL.md +80 -0
- package/dist/mcp-skills/deepflow-mcp-jira-write/SKILL.md +74 -0
- package/dist/mcp-skills/deepflow-mcp-playwright-read/SKILL.md +79 -0
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +65 -0
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +98 -0
- package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +62 -0
- package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +63 -0
- package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +80 -0
- package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +74 -0
- package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +79 -0
- package/dist/registry.json +818 -0
- package/dist/skills/atoll-ops/SKILL.md +46 -0
- package/dist/skills/reef-commit/SKILL.md +127 -0
- package/dist/skills/reef-gen-backend/SKILL.md.tmpl +87 -0
- package/dist/skills/reef-gen-backend/variants/java/steps.md +28 -0
- package/dist/skills/reef-gen-backend/variants/python/steps.md +70 -0
- package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +83 -0
- package/dist/skills/reef-gen-frontend/variants/angular/steps.md +30 -0
- package/dist/skills/reef-harden/EXAMPLES.md +89 -0
- package/dist/skills/reef-harden/SKILL.md +136 -0
- package/dist/skills/reef-pr/SKILL.md +97 -0
- package/dist/skills/reef-review/SKILL.md.tmpl +107 -0
- package/dist/skills/reef-scope/SKILL.md +134 -0
- package/dist/skills/reef-start/SKILL.md.tmpl +562 -0
- package/dist/skills/reef-start/references/jira-start-subagent.md +60 -0
- package/dist/skills/reef-style-backend/SKILL.md.tmpl +134 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/chat-client.md +96 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/rag.md +94 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +62 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/tool-calling.md +68 -0
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +220 -0
- package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +148 -0
- package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +131 -0
- package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/quick-reference.md +103 -0
- package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +119 -0
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +101 -0
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +181 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/controller.md +95 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +121 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/infrastructure.md +179 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +202 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +107 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +150 -0
- package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +197 -0
- package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/examples/user-repository-test.md +69 -0
- package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +101 -0
- package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md +61 -0
- package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +100 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/examples/user-controller-test.md +61 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +85 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/examples/user-service-integration-test.md +56 -0
- package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +77 -0
- package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +164 -0
- package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +139 -0
- package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +177 -0
- package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +101 -0
- package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +135 -0
- package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +111 -0
- package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +83 -0
- package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +207 -0
- package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +91 -0
- package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +227 -0
- package/dist/skills/reef-style-backend/variants/java/examples/contributor-pattern.md +97 -0
- package/dist/skills/reef-style-backend/variants/java/quick-reference.md +117 -0
- package/dist/skills/reef-style-backend/variants/python/examples/crud-router.md +71 -0
- package/dist/skills/reef-style-backend/variants/python/examples/pydantic-schema.md +45 -0
- package/dist/skills/reef-style-backend/variants/python/examples/pytest-fixture.md +60 -0
- package/dist/skills/reef-style-backend/variants/python/examples/sqlalchemy-model.md +31 -0
- package/dist/skills/reef-style-backend/variants/python/quick-reference.md +111 -0
- package/dist/skills/reef-style-frontend/SKILL.md.tmpl +70 -0
- package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +79 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +150 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +141 -0
- package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +112 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +134 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/quick-reference.md +66 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +252 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +56 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +100 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +119 -0
- package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +95 -0
- package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +100 -0
- package/dist/skills/reef-testcase/SKILL.md +138 -0
- package/dist/skills/reef-testcase/references/coverage-dimensions.md +99 -0
- package/dist/skills/reef-testcase/references/test-case-template.md +63 -0
- package/dist/skills/sweep-init/SKILL.md +286 -0
- package/dist/skills/sweep-init/scripts/flow-selector.mjs +305 -0
- package/dist/skills/sweep-plan/SKILL.md.tmpl +312 -0
- package/dist/skills/sweep-plan/references/test-flow-template.md +46 -0
- package/dist/skills/sweep-run/SKILL.md +437 -0
- package/dist/skills/sweep-run/scripts/env-manager.mjs +208 -0
- package/dist/skills/sweep-run/scripts/flow-parser.mjs +329 -0
- package/dist/skills/sweep-run/scripts/flow-selector.mjs +483 -0
- package/dist/skills/sweep-run/scripts/mcp-manager.mjs +208 -0
- package/dist/skills/sweep-run/scripts/spec-compiler.mjs +303 -0
- package/dist/skills/tide-discuss/SKILL.md.tmpl +449 -0
- package/dist/skills/tide-discuss/references/checklists.md +88 -0
- package/dist/skills/tide-discuss/references/data-format.md +237 -0
- package/dist/skills/tide-discuss/references/prd-template.md +134 -0
- package/dist/skills/tide-discuss/references/publish-flow.md +167 -0
- package/dist/skills/tide-discuss/references/role-prompts.md +105 -0
- 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 序列化。
|
package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md
ADDED
|
@@ -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` 确保每个测试回滚,互不干扰
|
package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md
ADDED
|
@@ -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` 查询)
|