@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,56 @@
|
|
|
1
|
+
```java
|
|
2
|
+
@SpringBootTest
|
|
3
|
+
@Transactional
|
|
4
|
+
@ActiveProfiles("test")
|
|
5
|
+
@Testcontainers
|
|
6
|
+
class UserServiceIntegrationTest {
|
|
7
|
+
|
|
8
|
+
@Container @ServiceConnection
|
|
9
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
|
|
10
|
+
|
|
11
|
+
@Autowired private UserService userService;
|
|
12
|
+
|
|
13
|
+
// ──── 读 ────
|
|
14
|
+
|
|
15
|
+
@Test @Sql("/user-service-test-data.sql")
|
|
16
|
+
void should_returnAllUsers() {
|
|
17
|
+
assertThat(userService.findAll())
|
|
18
|
+
.extracting(UserDTO::email)
|
|
19
|
+
.containsExactlyInAnyOrder("alice@test.com", "bob@test.com");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
void should_throw_when_userNotFound() {
|
|
24
|
+
assertThatThrownBy(() -> userService.findById(999L))
|
|
25
|
+
.isInstanceOf(NotFoundException.class);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ──── 写 ────
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
void should_createUser() {
|
|
32
|
+
UserDTO result = userService.create(new CreateUserRequest("new@test.com", "New User"));
|
|
33
|
+
assertThat(result.id()).isNotNull();
|
|
34
|
+
assertThat(result.email()).isEqualTo("new@test.com");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
void should_throw_when_duplicateEmail() {
|
|
39
|
+
userService.create(new CreateUserRequest("dup@test.com", "First"));
|
|
40
|
+
assertThatThrownBy(() ->
|
|
41
|
+
userService.create(new CreateUserRequest("dup@test.com", "Second")))
|
|
42
|
+
.isInstanceOf(AlreadyExistsException.class);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ──── 事务 ────
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
void should_rollback_on_failure() {
|
|
49
|
+
userService.create(new CreateUserRequest("a@test.com", "A"));
|
|
50
|
+
assertThatThrownBy(() ->
|
|
51
|
+
userService.create(new CreateUserRequest("a@test.com", "A Duplicate")))
|
|
52
|
+
.isInstanceOf(AlreadyExistsException.class);
|
|
53
|
+
// @Transactional 自动回滚,数据库只有一条 a@test.com
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Spring Service 测试规范
|
|
2
|
+
|
|
3
|
+
`@SpringBootTest` + `@Transactional`:加载完整上下文,每个测试结束后事务回滚,互不影响。
|
|
4
|
+
|
|
5
|
+
## 基础结构
|
|
6
|
+
|
|
7
|
+
```java
|
|
8
|
+
@SpringBootTest
|
|
9
|
+
@Transactional
|
|
10
|
+
@ActiveProfiles("test")
|
|
11
|
+
class UserServiceTest {
|
|
12
|
+
@Autowired private UserService userService;
|
|
13
|
+
@Autowired private TestEntityManager entityManager; // 直接操作数据库准备数据
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **⚠️ 何时用 `@SpringBootTest` vs `@ExtendWith(MockitoExtension.class)`**
|
|
18
|
+
> - **数据库交互**(Repository / 事务)→ `@SpringBootTest`
|
|
19
|
+
> - **纯业务逻辑**(计算、校验、映射)→ `MockitoExtension`(更快)
|
|
20
|
+
|
|
21
|
+
## 测试模式
|
|
22
|
+
|
|
23
|
+
### 读操作
|
|
24
|
+
|
|
25
|
+
```java
|
|
26
|
+
@Test
|
|
27
|
+
void should_returnUser_when_findById() {
|
|
28
|
+
User user = entityManager.persistAndFlush(new User("test@test.com", "Test"));
|
|
29
|
+
UserDTO result = userService.findById(user.getId());
|
|
30
|
+
assertThat(result).extracting(UserDTO::id, UserDTO::email)
|
|
31
|
+
.containsExactly(user.getId(), "test@test.com");
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 写操作
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
@Test
|
|
39
|
+
void should_createUser() {
|
|
40
|
+
UserDTO result = userService.create(new CreateUserRequest("new@test.com", "New"));
|
|
41
|
+
assertThat(result.id()).isNotNull();
|
|
42
|
+
// 验证数据库真实写入
|
|
43
|
+
User saved = entityManager.find(User.class, result.id());
|
|
44
|
+
assertThat(saved).isNotNull();
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 异常路径
|
|
49
|
+
|
|
50
|
+
```java
|
|
51
|
+
@Test
|
|
52
|
+
void should_throw_when_duplicateEmail() {
|
|
53
|
+
entityManager.persistAndFlush(new User("dup@test.com", "Existing"));
|
|
54
|
+
assertThatThrownBy(() -> userService.create(
|
|
55
|
+
new CreateUserRequest("dup@test.com", "Duplicate")))
|
|
56
|
+
.isInstanceOf(AlreadyExistsException.class);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 事务回滚
|
|
61
|
+
|
|
62
|
+
```java
|
|
63
|
+
@Test
|
|
64
|
+
void should_rollback_on_failure() {
|
|
65
|
+
userService.create(new CreateUserRequest("a@test.com", "A"));
|
|
66
|
+
assertThatThrownBy(() ->
|
|
67
|
+
userService.create(new CreateUserRequest("a@test.com", "A2")))
|
|
68
|
+
.isInstanceOf(AlreadyExistsException.class);
|
|
69
|
+
// @Transactional 自动回滚,数据库只有一条 a@test.com
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 关键规则
|
|
74
|
+
|
|
75
|
+
| 场景 | 做法 |
|
|
76
|
+
|------|------|
|
|
77
|
+
| 需要数据库交互 | `@SpringBootTest` + `TestEntityManager` |
|
|
78
|
+
| 纯业务逻辑 | `@ExtendWith(MockitoExtension.class)` |
|
|
79
|
+
| 测试隔离 | `@Transactional`(自动回滚) |
|
|
80
|
+
| 测试配置 | `@ActiveProfiles("test")` |
|
|
81
|
+
| 数据准备 | `entityManager.persistAndFlush()` 或 `@Sql` |
|
|
82
|
+
|
|
83
|
+
> **注意:** `@Transactional` 不回滚 `@PostConstruct` 和异步操作(`@Async`)中的操作。
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Alembic 迁移实践
|
|
2
|
+
|
|
3
|
+
## 初始化
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# 初始化 Alembic
|
|
7
|
+
alembic init -t async migrations
|
|
8
|
+
|
|
9
|
+
# 配置 async 引擎(migrations/env.py 中)
|
|
10
|
+
# target_metadata = Base.metadata
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 创建迁移
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# ✅ 自动生成迁移(推荐)
|
|
17
|
+
alembic revision --autogenerate -m "add user table"
|
|
18
|
+
|
|
19
|
+
# ⚠️ 检查自动生成的内容是否正确后再提交
|
|
20
|
+
# 自动生成不能 100% 检测到所有变更(如列重命名)
|
|
21
|
+
|
|
22
|
+
# ✅ 手动创建空迁移(特殊场景)
|
|
23
|
+
alembic revision -m "migrate data"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 应用迁移
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 升级到最新
|
|
30
|
+
alembic upgrade head
|
|
31
|
+
|
|
32
|
+
# 回滚一个版本
|
|
33
|
+
alembic downgrade -1
|
|
34
|
+
|
|
35
|
+
# 查看历史
|
|
36
|
+
alembic history
|
|
37
|
+
|
|
38
|
+
# 查看当前版本
|
|
39
|
+
alembic current
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 迁移文件规范
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
"""add user table
|
|
46
|
+
|
|
47
|
+
Revision ID: abc123
|
|
48
|
+
Revises:
|
|
49
|
+
Create Date: 2026-06-25 10:00:00
|
|
50
|
+
"""
|
|
51
|
+
from alembic import op
|
|
52
|
+
import sqlalchemy as sa
|
|
53
|
+
|
|
54
|
+
# revision identifiers
|
|
55
|
+
revision = "abc123"
|
|
56
|
+
down_revision = None
|
|
57
|
+
|
|
58
|
+
def upgrade() -> None:
|
|
59
|
+
op.create_table(
|
|
60
|
+
"users",
|
|
61
|
+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
62
|
+
sa.Column("name", sa.String(length=100), nullable=False),
|
|
63
|
+
sa.Column("email", sa.String(length=255), nullable=False),
|
|
64
|
+
sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()),
|
|
65
|
+
sa.PrimaryKeyConstraint("id"),
|
|
66
|
+
sa.UniqueConstraint("email"),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def downgrade() -> None:
|
|
70
|
+
op.drop_table("users")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**规范:**
|
|
74
|
+
- 每个迁移文件只做**一件事**(一个表或一个字段变更)
|
|
75
|
+
- `upgrade()` 和 `downgrade()` 必须互逆
|
|
76
|
+
- `down_revision` 必须正确指向前一个迁移
|
|
77
|
+
- **禁止**在生产环境直接执行未经 review 的自动生成迁移
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# API 规范(Python / FastAPI)
|
|
2
|
+
|
|
3
|
+
## 速查
|
|
4
|
+
|
|
5
|
+
| 场景 | 决策 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 资源路径 | 英文复数 kebab-case:`/api/v1/user-roles`,不用 camelCase |
|
|
8
|
+
| 路径变量 | snake_case:`/api/v1/users/{user_id}` |
|
|
9
|
+
| 自定义动作 | 遵循 REST 语义;无法用标准 CRUD 时用 `@router.post("/{id}:action")` |
|
|
10
|
+
| 查询参数 | snake_case:`?status=active&page=1&page_size=20` |
|
|
11
|
+
| 统一响应体 | `response_model` + `ApiResponse[T]` / `PageResponse[T]` 包装 |
|
|
12
|
+
| 版本策略 | URL path 前缀 `/api/v1/` |
|
|
13
|
+
| 错误响应 | 统一 JSON:`{"code": "USER_001", "message": "...", "detail": {}}` |
|
|
14
|
+
| OpenAPI | FastAPI 自动生成,配合 `summary` / `description` / `tags` |
|
|
15
|
+
| 分页 | `page`(1-based) + `page_size`(默认 20,最大 100) |
|
|
16
|
+
|
|
17
|
+
## 核心规范
|
|
18
|
+
|
|
19
|
+
### 资源命名
|
|
20
|
+
|
|
21
|
+
- **资源用复数**:`/users`、`/orders`
|
|
22
|
+
- **嵌套资源**:`/apps/{app_id}/users/{user_id}`
|
|
23
|
+
- **路径 kebab-case**:/api/v1/user-roles,不是 `/api/v1/userRoles`
|
|
24
|
+
- **路径参数 snake_case**:`{user_id}`,不是 `{userId}`
|
|
25
|
+
- **查询参数 snake_case**:`created_before`、`sort_by`
|
|
26
|
+
|
|
27
|
+
### 统一响应体
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from pydantic import BaseModel
|
|
31
|
+
from typing import Generic, TypeVar, List, Optional
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ✅ 单资源响应
|
|
37
|
+
class ApiResponse(BaseModel, Generic[T]):
|
|
38
|
+
success: bool
|
|
39
|
+
data: Optional[T] = None
|
|
40
|
+
message: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def success(data: T) -> "ApiResponse[T]":
|
|
44
|
+
return ApiResponse(success=True, data=data)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def message(msg: str) -> "ApiResponse[T]":
|
|
48
|
+
return ApiResponse(success=True, message=msg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ✅ 分页响应
|
|
52
|
+
class PageResponse(BaseModel, Generic[T]):
|
|
53
|
+
items: List[T]
|
|
54
|
+
page: int
|
|
55
|
+
page_size: int
|
|
56
|
+
total: int
|
|
57
|
+
total_pages: int
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ✅ 错误响应
|
|
61
|
+
class ErrorResponse(BaseModel):
|
|
62
|
+
code: str
|
|
63
|
+
message: str
|
|
64
|
+
detail: dict = {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# ✅ 好:统一响应
|
|
69
|
+
from fastapi import APIRouter
|
|
70
|
+
|
|
71
|
+
router = APIRouter(prefix="/api/v1/users", tags=["users"])
|
|
72
|
+
|
|
73
|
+
@router.get("/{user_id}", response_model=ApiResponse[UserResponse])
|
|
74
|
+
async def get_user(user_id: int, user_service: UserService = Depends()):
|
|
75
|
+
user = await user_service.get_user(user_id)
|
|
76
|
+
return ApiResponse.success(user)
|
|
77
|
+
|
|
78
|
+
@router.get("", response_model=PageResponse[UserResponse])
|
|
79
|
+
async def list_users(
|
|
80
|
+
page: int = Query(1, ge=1),
|
|
81
|
+
page_size: int = Query(20, ge=1, le=100),
|
|
82
|
+
user_service: UserService = Depends(),
|
|
83
|
+
):
|
|
84
|
+
return await user_service.list_users(page=page, page_size=page_size)
|
|
85
|
+
|
|
86
|
+
# ❌ 坏:直接返回实体
|
|
87
|
+
@router.get("/{user_id}")
|
|
88
|
+
async def get_user(user_id: int):
|
|
89
|
+
return await user_service.get_user(user_id) # ← 未包装,格式不一致
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 版本策略
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# ✅ 好:按版本分组
|
|
96
|
+
# app/api/v1/users.py
|
|
97
|
+
router_v1 = APIRouter(prefix="/api/v1/users", tags=["users-v1"])
|
|
98
|
+
|
|
99
|
+
# app/api/v2/users.py
|
|
100
|
+
router_v2 = APIRouter(prefix="/api/v2/users", tags=["users-v2"])
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **非破坏性变更**(新增字段、新增可选参数):停留在当前版本
|
|
104
|
+
- **破坏性变更**(重命名字段、删除字段):创建 `/api/v2/...`
|
|
105
|
+
- **废弃端点**:`@router.get("/...", deprecated=True)`
|
|
106
|
+
|
|
107
|
+
### 分页约定
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# ✅ 好:标准分页
|
|
111
|
+
class PaginationParams:
|
|
112
|
+
"""分页参数依赖注入"""
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
page: int = Query(1, ge=1, description="页码(从1开始)"),
|
|
116
|
+
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
|
117
|
+
):
|
|
118
|
+
self.page = page
|
|
119
|
+
self.page_size = page_size
|
|
120
|
+
self.offset = (page - 1) * page_size
|
|
121
|
+
|
|
122
|
+
@router.get("", response_model=PageResponse[UserResponse])
|
|
123
|
+
async def list_users(
|
|
124
|
+
pagination: PaginationParams = Depends(),
|
|
125
|
+
user_service: UserService = Depends(),
|
|
126
|
+
):
|
|
127
|
+
items, total = await user_service.list_users(
|
|
128
|
+
skip=pagination.offset, limit=pagination.page_size
|
|
129
|
+
)
|
|
130
|
+
return PageResponse(
|
|
131
|
+
items=items, page=pagination.page,
|
|
132
|
+
page_size=pagination.page_size, total=total,
|
|
133
|
+
total_pages=-(-total // pagination.page_size), # ceil division
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### OpenAPI 文档
|
|
138
|
+
|
|
139
|
+
FastAPI 自动基于 Pydantic model + 路由生成 OpenAPI。额外配置:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# ✅ 好:在路由上添加接口描述
|
|
143
|
+
@router.get(
|
|
144
|
+
"/{user_id}",
|
|
145
|
+
summary="获取用户详情",
|
|
146
|
+
description="根据用户 ID 获取完整的用户信息",
|
|
147
|
+
response_model=ApiResponse[UserResponse],
|
|
148
|
+
responses={
|
|
149
|
+
404: {"description": "用户不存在", "model": ErrorResponse},
|
|
150
|
+
403: {"description": "无权限", "model": ErrorResponse},
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
async def get_user(user_id: int, ...):
|
|
154
|
+
...
|
|
155
|
+
|
|
156
|
+
# ✅ 好:全局 OpenAPI 元信息
|
|
157
|
+
app = FastAPI(
|
|
158
|
+
title="My App API",
|
|
159
|
+
description="用户管理系统 API",
|
|
160
|
+
version="1.0.0",
|
|
161
|
+
docs_url="/api/docs" if settings.ENV != "production" else None,
|
|
162
|
+
redoc_url=None,
|
|
163
|
+
)
|
|
164
|
+
```
|
package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# 依赖管理规范(Python)
|
|
2
|
+
|
|
3
|
+
## 速查
|
|
4
|
+
|
|
5
|
+
| 场景 | 决策 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 包管理器 | `uv`(统一管理和运行) |
|
|
8
|
+
| 声明依赖 | `uv add {package}` 或直接在 `pyproject.toml` 中声明 |
|
|
9
|
+
| 版本声明 | 在 `pyproject.toml` 中指定主版本范围:`>=1.0,<2.0` |
|
|
10
|
+
| 禁止通配符版本 | 禁止 `*` 或 `>=0.0.0` |
|
|
11
|
+
| 禁止硬编码 | 禁止 `pip install package==1.0.0`(不可复现) |
|
|
12
|
+
| 依赖分组 | `[project.dependencies]`(运行)+ `[project.optional-dependencies]`(dev) |
|
|
13
|
+
| 锁定文件 | `uv.lock` — 提交到 Git |
|
|
14
|
+
| CVE 检查 | `uv audit` 扫描已知漏洞 |
|
|
15
|
+
| 版本升级 | 手动升级后运行 `uv lock` 更新锁定文件 |
|
|
16
|
+
|
|
17
|
+
## 核心规范
|
|
18
|
+
|
|
19
|
+
### pyproject.toml 结构
|
|
20
|
+
|
|
21
|
+
```toml
|
|
22
|
+
[project]
|
|
23
|
+
name = "my-app"
|
|
24
|
+
version = "0.1.0"
|
|
25
|
+
requires-python = ">=3.10"
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"fastapi>=0.115.0,<1.0.0",
|
|
29
|
+
"sqlalchemy>=2.0,<3.0",
|
|
30
|
+
"alembic>=1.14,<2.0",
|
|
31
|
+
"pydantic>=2.10,<3.0",
|
|
32
|
+
"pydantic-settings>=2.7,<3.0",
|
|
33
|
+
"httpx>=0.28,<1.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8.0,<9.0",
|
|
39
|
+
"pytest-asyncio>=0.24,<1.0",
|
|
40
|
+
"ruff>=0.8,<1.0",
|
|
41
|
+
"mypy>=1.13,<2.0",
|
|
42
|
+
"httpx>=0.28,<1.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.ruff]
|
|
46
|
+
# ruff 配置...
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
# mypy 配置...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**规范:**
|
|
53
|
+
- 运行依赖在 `[project.dependencies]` 中声明
|
|
54
|
+
- 开发/测试依赖在 `[project.optional-dependencies]` 中分组
|
|
55
|
+
- 每组使用语义化版本范围:`>=min,<max`
|
|
56
|
+
- 同一生态的库保持版本一致(如 FastAPI + Pydantic 需兼容)
|
|
57
|
+
|
|
58
|
+
### 禁止通配符版本
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
# ❌ 坏:通配符或不限版本
|
|
62
|
+
dependencies = [
|
|
63
|
+
"fastapi=*",
|
|
64
|
+
"sqlalchemy>=0.0.0",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# ✅ 好:限制主版本范围
|
|
68
|
+
dependencies = [
|
|
69
|
+
"fastapi>=0.115.0,<1.0.0",
|
|
70
|
+
"sqlalchemy>=2.0,<3.0",
|
|
71
|
+
]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### uv 命令速查
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 添加依赖
|
|
78
|
+
uv add fastapi
|
|
79
|
+
|
|
80
|
+
# 添加开发依赖
|
|
81
|
+
uv add --dev pytest pytest-asyncio
|
|
82
|
+
|
|
83
|
+
# 移除依赖
|
|
84
|
+
uv remove fastapi
|
|
85
|
+
|
|
86
|
+
# 更新锁文件(升级所有兼容版本)
|
|
87
|
+
uv lock
|
|
88
|
+
|
|
89
|
+
# 升级特定包
|
|
90
|
+
uv lock --upgrade-package fastapi
|
|
91
|
+
|
|
92
|
+
# 安全审计
|
|
93
|
+
uv audit
|
|
94
|
+
|
|
95
|
+
# 查看依赖树
|
|
96
|
+
uv tree
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# ❌ 坏:直接 pip install(绕过 uv.lock)
|
|
101
|
+
pip install fastapi==0.115.0
|
|
102
|
+
|
|
103
|
+
# ✅ 好:用 uv 管理所有依赖
|
|
104
|
+
uv add fastapi
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 版本升级规则
|
|
108
|
+
|
|
109
|
+
| 升级类型 | 操作 | 验证 |
|
|
110
|
+
|---------|------|------|
|
|
111
|
+
| Major 升级 | 手动升级,独立 PR | 需检查 breaking changes + API 兼容性 |
|
|
112
|
+
| Minor 升级 | `uv lock --upgrade-package {pkg}` | 运行完整测试套件 |
|
|
113
|
+
| Patch 升级 | `uv lock --upgrade-package {pkg}` | 运行测试 + lint |
|
|
114
|
+
|
|
115
|
+
### 安全审计
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# 扫描 CVE
|
|
119
|
+
uv audit
|
|
120
|
+
|
|
121
|
+
# 输出示例:
|
|
122
|
+
# Found 2 vulnerabilities:
|
|
123
|
+
# - urllib3<2.2.3 (CVE-2024-37891, medium)
|
|
124
|
+
# - requests<2.32.0 (CVE-2024-35195, medium)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**规范:**
|
|
128
|
+
- 每次 CI 中运行 `uv audit`
|
|
129
|
+
- 紧急 CVE(CVSS >= 7.0):1 天内升级并验证
|
|
130
|
+
- 无法立即升级时,加备注和缓解措施文档
|
|
131
|
+
|
|
132
|
+
### 已知 CVE 常见的 Python 包
|
|
133
|
+
|
|
134
|
+
| 包 | 受影响版本 | 最低安全版本 |
|
|
135
|
+
|---|----------|------------|
|
|
136
|
+
| urllib3 | < 2.2.3 | >= 2.2.3 |
|
|
137
|
+
| requests | < 2.32.0 | >= 2.32.0 |
|
|
138
|
+
| Jinja2 | < 3.1.5 | >= 3.1.5 |
|
|
139
|
+
| cryptography | < 43.0.1 | >= 43.0.1 |
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# 异常处理规范(Python / FastAPI)
|
|
2
|
+
|
|
3
|
+
## 速查
|
|
4
|
+
|
|
5
|
+
| 场景 | 决策 |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| 业务异常 | 继承 `AppError(HTTPException)`,含 `code` + `status_code` + `detail` |
|
|
8
|
+
| 全局捕获 | `@app.exception_handler(AppError)` 统一处理 |
|
|
9
|
+
| 错误响应 | `{"code": "USER_001", "message": "...", "detail": {}}` |
|
|
10
|
+
| 错误码 | `{MODULE}_{NNN}` 枚举 |
|
|
11
|
+
| 参数校验失败 | FastAPI 自动处理 `RequestValidationError` |
|
|
12
|
+
| 未知异常 | 兜底 500 — `GENERIC_001`,记录完整 traceback |
|
|
13
|
+
| Service 层异常 | 抛出 `AppError` 子类,不在 Router 中 try-catch |
|
|
14
|
+
|
|
15
|
+
## 核心规范
|
|
16
|
+
|
|
17
|
+
### 业务异常继承层次
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
HTTPException (fastapi)
|
|
21
|
+
└── AppError ← 抽象基类
|
|
22
|
+
├── NotFoundError (404)
|
|
23
|
+
├── ConflictError (409)
|
|
24
|
+
├── ValidationError (400)
|
|
25
|
+
├── PermissionError (403)
|
|
26
|
+
└── FailedPreconditionError (400)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from fastapi import HTTPException
|
|
31
|
+
from starlette.status import *
|
|
32
|
+
|
|
33
|
+
class AppError(HTTPException):
|
|
34
|
+
"""业务异常基类。所有业务异常必须继承此类。"""
|
|
35
|
+
def __init__(self, status_code: int, code: str, message: str, detail: dict | None = None):
|
|
36
|
+
super().__init__(status_code=status_code, detail={
|
|
37
|
+
"code": code,
|
|
38
|
+
"message": message,
|
|
39
|
+
"detail": detail or {},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
# 子类
|
|
43
|
+
class NotFoundError(AppError):
|
|
44
|
+
def __init__(self, code: str, resource_type: str, resource_id: any):
|
|
45
|
+
super().__init__(
|
|
46
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
47
|
+
code=code,
|
|
48
|
+
message=f"{resource_type} not found: {resource_id}",
|
|
49
|
+
detail={"resource_type": resource_type, "resource_id": resource_id},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
class ConflictError(AppError):
|
|
53
|
+
def __init__(self, code: str, message: str):
|
|
54
|
+
super().__init__(status_code=HTTP_409_CONFLICT, code=code, message=message)
|
|
55
|
+
|
|
56
|
+
class ValidationError(AppError):
|
|
57
|
+
def __init__(self, code: str, message: str, errors: list[dict] | None = None):
|
|
58
|
+
super().__init__(
|
|
59
|
+
status_code=HTTP_400_BAD_REQUEST,
|
|
60
|
+
code=code,
|
|
61
|
+
message=message,
|
|
62
|
+
detail={"errors": errors} if errors else {},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
class PermissionDeniedError(AppError):
|
|
66
|
+
def __init__(self, code: str = "AUTH_201", message: str = "无权限访问"):
|
|
67
|
+
super().__init__(status_code=HTTP_403_FORBIDDEN, code=code, message=message)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 错误码枚举
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from enum import Enum
|
|
74
|
+
|
|
75
|
+
class ErrorCode(str, Enum):
|
|
76
|
+
# User — 001~099 输入验证, 100~199 资源状态, 200~299 权限
|
|
77
|
+
USER_001 = "USER_001" # 用户名已存在
|
|
78
|
+
USER_002 = "USER_002" # 邮箱格式无效
|
|
79
|
+
USER_100 = "USER_100" # 用户不存在
|
|
80
|
+
USER_101 = "USER_101" # 用户已禁用
|
|
81
|
+
USER_200 = "USER_200" # 无权操作该用户
|
|
82
|
+
|
|
83
|
+
# Order
|
|
84
|
+
ORDER_001 = "ORDER_001" # 订单金额无效
|
|
85
|
+
ORDER_100 = "ORDER_100" # 订单不存在
|
|
86
|
+
ORDER_101 = "ORDER_101" # 订单状态不允许操作
|
|
87
|
+
|
|
88
|
+
# Auth
|
|
89
|
+
AUTH_001 = "AUTH_001" # Token 已过期
|
|
90
|
+
AUTH_002 = "AUTH_002" # Token 无效
|
|
91
|
+
AUTH_201 = "AUTH_201" # 无权限访问
|
|
92
|
+
|
|
93
|
+
# Generic
|
|
94
|
+
GENERIC_001 = "GENERIC_001" # 系统内部错误
|
|
95
|
+
GENERIC_404 = "GENERIC_404" # 接口不存在
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 全局异常处理器
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from fastapi import FastAPI, Request
|
|
102
|
+
from fastapi.responses import JSONResponse
|
|
103
|
+
from fastapi.exceptions import RequestValidationError
|
|
104
|
+
|
|
105
|
+
app = FastAPI()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ✅ 业务异常统一处理
|
|
109
|
+
@app.exception_handler(AppError)
|
|
110
|
+
async def app_error_handler(request: Request, exc: AppError):
|
|
111
|
+
return JSONResponse(
|
|
112
|
+
status_code=exc.status_code,
|
|
113
|
+
content=exc.detail,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ✅ FastAPI 参数校验失败(Pydantic 校验)
|
|
118
|
+
@app.exception_handler(RequestValidationError)
|
|
119
|
+
async def validation_error_handler(request: Request, exc: RequestValidationError):
|
|
120
|
+
errors = [
|
|
121
|
+
{"field": err["loc"][-1], "message": err["msg"]}
|
|
122
|
+
for err in exc.errors()
|
|
123
|
+
]
|
|
124
|
+
return JSONResponse(
|
|
125
|
+
status_code=422,
|
|
126
|
+
content={
|
|
127
|
+
"code": "VALIDATION_001",
|
|
128
|
+
"message": "参数校验失败",
|
|
129
|
+
"detail": {"errors": errors},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ✅ 兜底:未捕获异常 → 500
|
|
135
|
+
@app.exception_handler(Exception)
|
|
136
|
+
async def unexpected_error_handler(request: Request, exc: Exception):
|
|
137
|
+
logger.error("Unexpected error", exc_info=True) # 完整 traceback 只打日志
|
|
138
|
+
return JSONResponse(
|
|
139
|
+
status_code=500,
|
|
140
|
+
content={
|
|
141
|
+
"code": "GENERIC_001",
|
|
142
|
+
"message": "系统内部错误",
|
|
143
|
+
"detail": {},
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**规范:**
|
|
149
|
+
- 不要在 Router 函数中写 try-except 包裹业务逻辑
|
|
150
|
+
- 不要吞异常后返回 `None` 或空字典
|
|
151
|
+
- 兜底 handler 必须 `logger.error(..., exc_info=True)`
|
|
152
|
+
- `RequestValidationError` 的 detail 中返回具体字段错误信息
|
|
153
|
+
|
|
154
|
+
### 异常使用示例
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Service 层 — 直接抛异常
|
|
158
|
+
class UserService:
|
|
159
|
+
async def get_user(self, user_id: int) -> UserResponse:
|
|
160
|
+
user = await user_repository.get_by_id(user_id)
|
|
161
|
+
if user is None:
|
|
162
|
+
raise NotFoundError(ErrorCode.USER_100, "User", user_id)
|
|
163
|
+
return UserResponse.from_orm(user)
|
|
164
|
+
|
|
165
|
+
async def create_user(self, request: CreateUserRequest) -> UserResponse:
|
|
166
|
+
existing = await user_repository.get_by_email(request.email)
|
|
167
|
+
if existing:
|
|
168
|
+
raise ConflictError(ErrorCode.USER_001, "邮箱已注册")
|
|
169
|
+
user = await user_repository.create(request)
|
|
170
|
+
return UserResponse.from_orm(user)
|
|
171
|
+
|
|
172
|
+
# Router 层 — 不需要 try-catch
|
|
173
|
+
@router.post("", response_model=ApiResponse[UserResponse])
|
|
174
|
+
async def create_user(request: CreateUserRequest, user_service: UserService = Depends()):
|
|
175
|
+
user = await user_service.create_user(request)
|
|
176
|
+
return ApiResponse.success(user)
|
|
177
|
+
```
|