mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-03 23:53:29 +08:00
docs(zh-CN): sync Chinese docs with latest upstream changes (#304)
* docs(zh-CN): sync Chinese docs with latest upstream changes * update --------- Co-authored-by: neo <neo.dowithless@gmail.com>
This commit is contained in:
308
docs/zh-CN/examples/django-api-CLAUDE.md
Normal file
308
docs/zh-CN/examples/django-api-CLAUDE.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Django REST API — 项目 CLAUDE.md
|
||||
|
||||
> 使用 PostgreSQL 和 Celery 的 Django REST Framework API 真实示例。
|
||||
> 将此复制到你的项目根目录并针对你的服务进行自定义。
|
||||
|
||||
## 项目概述
|
||||
|
||||
**技术栈:** Python 3.12+, Django 5.x, Django REST Framework, PostgreSQL, Celery + Redis, pytest, Docker Compose
|
||||
|
||||
**架构:** 采用领域驱动设计,每个业务领域对应一个应用。DRF 用于 API 层,Celery 用于异步任务,pytest 用于测试。所有端点返回 JSON — 无模板渲染。
|
||||
|
||||
## 关键规则
|
||||
|
||||
### Python 约定
|
||||
|
||||
* 所有函数签名使用类型提示 — 使用 `from __future__ import annotations`
|
||||
* 不使用 `print()` 语句 — 使用 `logging.getLogger(__name__)`
|
||||
* 字符串格式化使用 f-strings,绝不使用 `%` 或 `.format()`
|
||||
* 文件操作使用 `pathlib.Path` 而非 `os.path`
|
||||
* 导入排序使用 isort:标准库、第三方库、本地库(由 ruff 强制执行)
|
||||
|
||||
### 数据库
|
||||
|
||||
* 所有查询使用 Django ORM — 原始 SQL 仅与 `.raw()` 和参数化查询一起使用
|
||||
* 迁移文件提交到 git — 生产中绝不使用 `--fake`
|
||||
* 使用 `select_related()` 和 `prefetch_related()` 防止 N+1 查询
|
||||
* 所有模型必须具有 `created_at` 和 `updated_at` 自动字段
|
||||
* 在 `filter()`、`order_by()` 或 `WHERE` 子句中使用的任何字段上建立索引
|
||||
|
||||
```python
|
||||
# BAD: N+1 query
|
||||
orders = Order.objects.all()
|
||||
for order in orders:
|
||||
print(order.customer.name) # hits DB for each order
|
||||
|
||||
# GOOD: Single query with join
|
||||
orders = Order.objects.select_related("customer").all()
|
||||
```
|
||||
|
||||
### 认证
|
||||
|
||||
* 通过 `djangorestframework-simplejwt` 使用 JWT — 访问令牌(15 分钟)+ 刷新令牌(7 天)
|
||||
* 每个视图都设置权限类 — 绝不依赖默认设置
|
||||
* 使用 `IsAuthenticated` 作为基础,为对象级访问添加自定义权限
|
||||
* 为登出启用令牌黑名单
|
||||
|
||||
### 序列化器
|
||||
|
||||
* 简单 CRUD 使用 `ModelSerializer`,复杂验证使用 `Serializer`
|
||||
* 当输入/输出结构不同时,分离读写序列化器
|
||||
* 在序列化器层面进行验证,而非在视图中 — 视图应保持精简
|
||||
|
||||
```python
|
||||
class CreateOrderSerializer(serializers.Serializer):
|
||||
product_id = serializers.UUIDField()
|
||||
quantity = serializers.IntegerField(min_value=1, max_value=100)
|
||||
|
||||
def validate_product_id(self, value):
|
||||
if not Product.objects.filter(id=value, active=True).exists():
|
||||
raise serializers.ValidationError("Product not found or inactive")
|
||||
return value
|
||||
|
||||
class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
customer = CustomerSerializer(read_only=True)
|
||||
product = ProductSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ["id", "customer", "product", "quantity", "total", "status", "created_at"]
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
* 使用 DRF 异常处理器确保一致的错误响应
|
||||
* 业务逻辑中的自定义异常放在 `core/exceptions.py`
|
||||
* 绝不向客户端暴露内部错误细节
|
||||
|
||||
```python
|
||||
# core/exceptions.py
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
class InsufficientStockError(APIException):
|
||||
status_code = 409
|
||||
default_detail = "Insufficient stock for this order"
|
||||
default_code = "insufficient_stock"
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
* 代码或注释中不使用表情符号
|
||||
* 最大行长度:120 个字符(由 ruff 强制执行)
|
||||
* 类名:PascalCase,函数/变量名:snake\_case,常量:UPPER\_SNAKE\_CASE
|
||||
* 视图保持精简 — 业务逻辑放在服务函数或模型方法中
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
config/
|
||||
settings/
|
||||
base.py # Shared settings
|
||||
local.py # Dev overrides (DEBUG=True)
|
||||
production.py # Production settings
|
||||
urls.py # Root URL config
|
||||
celery.py # Celery app configuration
|
||||
apps/
|
||||
accounts/ # User auth, registration, profile
|
||||
models.py
|
||||
serializers.py
|
||||
views.py
|
||||
services.py # Business logic
|
||||
tests/
|
||||
test_views.py
|
||||
test_services.py
|
||||
factories.py # Factory Boy factories
|
||||
orders/ # Order management
|
||||
models.py
|
||||
serializers.py
|
||||
views.py
|
||||
services.py
|
||||
tasks.py # Celery tasks
|
||||
tests/
|
||||
products/ # Product catalog
|
||||
models.py
|
||||
serializers.py
|
||||
views.py
|
||||
tests/
|
||||
core/
|
||||
exceptions.py # Custom API exceptions
|
||||
permissions.py # Shared permission classes
|
||||
pagination.py # Custom pagination
|
||||
middleware.py # Request logging, timing
|
||||
tests/
|
||||
```
|
||||
|
||||
## 关键模式
|
||||
|
||||
### 服务层
|
||||
|
||||
```python
|
||||
# apps/orders/services.py
|
||||
from django.db import transaction
|
||||
|
||||
def create_order(*, customer, product_id: uuid.UUID, quantity: int) -> Order:
|
||||
"""Create an order with stock validation and payment hold."""
|
||||
product = Product.objects.select_for_update().get(id=product_id)
|
||||
|
||||
if product.stock < quantity:
|
||||
raise InsufficientStockError()
|
||||
|
||||
with transaction.atomic():
|
||||
order = Order.objects.create(
|
||||
customer=customer,
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
total=product.price * quantity,
|
||||
)
|
||||
product.stock -= quantity
|
||||
product.save(update_fields=["stock", "updated_at"])
|
||||
|
||||
# Async: send confirmation email
|
||||
send_order_confirmation.delay(order.id)
|
||||
return order
|
||||
```
|
||||
|
||||
### 视图模式
|
||||
|
||||
```python
|
||||
# apps/orders/views.py
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = StandardPagination
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return CreateOrderSerializer
|
||||
return OrderDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Order.objects
|
||||
.filter(customer=self.request.user)
|
||||
.select_related("product", "customer")
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
order = create_order(
|
||||
customer=self.request.user,
|
||||
product_id=serializer.validated_data["product_id"],
|
||||
quantity=serializer.validated_data["quantity"],
|
||||
)
|
||||
serializer.instance = order
|
||||
```
|
||||
|
||||
### 测试模式 (pytest + Factory Boy)
|
||||
|
||||
```python
|
||||
# apps/orders/tests/factories.py
|
||||
import factory
|
||||
from apps.accounts.tests.factories import UserFactory
|
||||
from apps.products.tests.factories import ProductFactory
|
||||
|
||||
class OrderFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = "orders.Order"
|
||||
|
||||
customer = factory.SubFactory(UserFactory)
|
||||
product = factory.SubFactory(ProductFactory, stock=100)
|
||||
quantity = 1
|
||||
total = factory.LazyAttribute(lambda o: o.product.price * o.quantity)
|
||||
|
||||
# apps/orders/tests/test_views.py
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestCreateOrder:
|
||||
def setup_method(self):
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory()
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
def test_create_order_success(self):
|
||||
product = ProductFactory(price=29_99, stock=10)
|
||||
response = self.client.post("/api/orders/", {
|
||||
"product_id": str(product.id),
|
||||
"quantity": 2,
|
||||
})
|
||||
assert response.status_code == 201
|
||||
assert response.data["total"] == 59_98
|
||||
|
||||
def test_create_order_insufficient_stock(self):
|
||||
product = ProductFactory(stock=0)
|
||||
response = self.client.post("/api/orders/", {
|
||||
"product_id": str(product.id),
|
||||
"quantity": 1,
|
||||
})
|
||||
assert response.status_code == 409
|
||||
|
||||
def test_create_order_unauthenticated(self):
|
||||
self.client.force_authenticate(None)
|
||||
response = self.client.post("/api/orders/", {})
|
||||
assert response.status_code == 401
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
```bash
|
||||
# Django
|
||||
SECRET_KEY=
|
||||
DEBUG=False
|
||||
ALLOWED_HOSTS=api.example.com
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
||||
|
||||
# Redis (Celery broker + cache)
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# JWT
|
||||
JWT_ACCESS_TOKEN_LIFETIME=15 # minutes
|
||||
JWT_REFRESH_TOKEN_LIFETIME=10080 # minutes (7 days)
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST=smtp.example.com
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest --cov=apps --cov-report=term-missing
|
||||
|
||||
# Run specific app tests
|
||||
pytest apps/orders/tests/ -v
|
||||
|
||||
# Run with parallel execution
|
||||
pytest -n auto
|
||||
|
||||
# Only failing tests from last run
|
||||
pytest --lf
|
||||
```
|
||||
|
||||
## ECC 工作流
|
||||
|
||||
```bash
|
||||
# Planning
|
||||
/plan "Add order refund system with Stripe integration"
|
||||
|
||||
# Development with TDD
|
||||
/tdd # pytest-based TDD workflow
|
||||
|
||||
# Review
|
||||
/python-review # Python-specific code review
|
||||
/security-scan # Django security audit
|
||||
/code-review # General quality check
|
||||
|
||||
# Verification
|
||||
/verify # Build, lint, test, security scan
|
||||
```
|
||||
|
||||
## Git 工作流
|
||||
|
||||
* `feat:` 新功能,`fix:` 错误修复,`refactor:` 代码变更
|
||||
* 功能分支从 `main` 创建,需要 PR
|
||||
* CI:ruff(代码检查 + 格式化)、mypy(类型检查)、pytest(测试)、safety(依赖检查)
|
||||
* 部署:Docker 镜像,通过 Kubernetes 或 Railway 管理
|
||||
267
docs/zh-CN/examples/go-microservice-CLAUDE.md
Normal file
267
docs/zh-CN/examples/go-microservice-CLAUDE.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# Go 微服务 — 项目 CLAUDE.md
|
||||
|
||||
> 一个使用 PostgreSQL、gRPC 和 Docker 的 Go 微服务真实示例。
|
||||
> 将此文件复制到您的项目根目录,并根据您的服务进行自定义。
|
||||
|
||||
## 项目概述
|
||||
|
||||
**技术栈:** Go 1.22+, PostgreSQL, gRPC + REST (grpc-gateway), Docker, sqlc (类型安全的 SQL), Wire (依赖注入)
|
||||
|
||||
**架构:** 采用领域、仓库、服务和处理器层的清晰架构。gRPC 作为主要传输方式,REST 网关用于外部客户端。
|
||||
|
||||
## 关键规则
|
||||
|
||||
### Go 规范
|
||||
|
||||
* 遵循 Effective Go 和 Go Code Review Comments 指南
|
||||
* 使用 `errors.New` / `fmt.Errorf` 配合 `%w` 进行包装 — 绝不对错误进行字符串匹配
|
||||
* 不使用 `init()` 函数 — 在 `main()` 或构造函数中进行显式初始化
|
||||
* 没有全局可变状态 — 通过构造函数传递依赖项
|
||||
* Context 必须是第一个参数,并在所有层中传播
|
||||
|
||||
### 数据库
|
||||
|
||||
* `queries/` 中的所有查询都使用纯 SQL — sqlc 生成类型安全的 Go 代码
|
||||
* 在 `migrations/` 中使用 golang-migrate 进行迁移 — 绝不直接更改数据库
|
||||
* 通过 `pgx.Tx` 为多步骤操作使用事务
|
||||
* 所有查询必须使用参数化占位符 (`$1`, `$2`) — 绝不使用字符串格式化
|
||||
|
||||
### 错误处理
|
||||
|
||||
* 返回错误,不要 panic — panic 仅用于真正无法恢复的情况
|
||||
* 使用上下文包装错误:`fmt.Errorf("creating user: %w", err)`
|
||||
* 在 `domain/errors.go` 中定义业务逻辑的哨兵错误
|
||||
* 在处理器层将领域错误映射到 gRPC 状态码
|
||||
|
||||
```go
|
||||
// Domain layer — sentinel errors
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrEmailTaken = errors.New("email already registered")
|
||||
)
|
||||
|
||||
// Handler layer — map to gRPC status
|
||||
func toGRPCError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrUserNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, domain.ErrEmailTaken):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
* 代码或注释中不使用表情符号
|
||||
* 导出的类型和函数必须有文档注释
|
||||
* 函数保持在 50 行以内 — 提取辅助函数
|
||||
* 对所有具有多个用例的逻辑使用表格驱动测试
|
||||
* 对于信号通道,优先使用 `struct{}`,而不是 `bool`
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
cmd/
|
||||
server/
|
||||
main.go # Entrypoint, Wire injection, graceful shutdown
|
||||
internal/
|
||||
domain/ # Business types and interfaces
|
||||
user.go # User entity and repository interface
|
||||
errors.go # Sentinel errors
|
||||
service/ # Business logic
|
||||
user_service.go
|
||||
user_service_test.go
|
||||
repository/ # Data access (sqlc-generated + custom)
|
||||
postgres/
|
||||
user_repo.go
|
||||
user_repo_test.go # Integration tests with testcontainers
|
||||
handler/ # gRPC + REST handlers
|
||||
grpc/
|
||||
user_handler.go
|
||||
rest/
|
||||
user_handler.go
|
||||
config/ # Configuration loading
|
||||
config.go
|
||||
proto/ # Protobuf definitions
|
||||
user/v1/
|
||||
user.proto
|
||||
queries/ # SQL queries for sqlc
|
||||
user.sql
|
||||
migrations/ # Database migrations
|
||||
001_create_users.up.sql
|
||||
001_create_users.down.sql
|
||||
```
|
||||
|
||||
## 关键模式
|
||||
|
||||
### 仓库接口
|
||||
|
||||
```go
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
FindByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
FindByEmail(ctx context.Context, email string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
```
|
||||
|
||||
### 使用依赖注入的服务
|
||||
|
||||
```go
|
||||
type UserService struct {
|
||||
repo domain.UserRepository
|
||||
hasher PasswordHasher
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger *slog.Logger) *UserService {
|
||||
return &UserService{repo: repo, hasher: hasher, logger: logger}
|
||||
}
|
||||
|
||||
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*domain.User, error) {
|
||||
existing, err := s.repo.FindByEmail(ctx, req.Email)
|
||||
if err != nil && !errors.Is(err, domain.ErrUserNotFound) {
|
||||
return nil, fmt.Errorf("checking email: %w", err)
|
||||
}
|
||||
if existing != nil {
|
||||
return nil, domain.ErrEmailTaken
|
||||
}
|
||||
|
||||
hashed, err := s.hasher.Hash(req.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hashing password: %w", err)
|
||||
}
|
||||
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: hashed,
|
||||
}
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 表格驱动测试
|
||||
|
||||
```go
|
||||
func TestUserService_Create(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req CreateUserRequest
|
||||
setup func(*MockUserRepo)
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "valid user",
|
||||
req: CreateUserRequest{Name: "Alice", Email: "alice@example.com", Password: "secure123"},
|
||||
setup: func(m *MockUserRepo) {
|
||||
m.On("FindByEmail", mock.Anything, "alice@example.com").Return(nil, domain.ErrUserNotFound)
|
||||
m.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "duplicate email",
|
||||
req: CreateUserRequest{Name: "Alice", Email: "taken@example.com", Password: "secure123"},
|
||||
setup: func(m *MockUserRepo) {
|
||||
m.On("FindByEmail", mock.Anything, "taken@example.com").Return(&domain.User{}, nil)
|
||||
},
|
||||
wantErr: domain.ErrEmailTaken,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := new(MockUserRepo)
|
||||
tt.setup(repo)
|
||||
svc := NewUserService(repo, &bcryptHasher{}, slog.Default())
|
||||
|
||||
_, err := svc.Create(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/myservice?sslmode=disable
|
||||
|
||||
# gRPC
|
||||
GRPC_PORT=50051
|
||||
REST_PORT=8080
|
||||
|
||||
# Auth
|
||||
JWT_SECRET= # Load from vault in production
|
||||
TOKEN_EXPIRY=24h
|
||||
|
||||
# Observability
|
||||
LOG_LEVEL=info # debug, info, warn, error
|
||||
OTEL_ENDPOINT= # OpenTelemetry collector
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
```bash
|
||||
/go-test # TDD workflow for Go
|
||||
/go-review # Go-specific code review
|
||||
/go-build # Fix build errors
|
||||
```
|
||||
|
||||
### 测试命令
|
||||
|
||||
```bash
|
||||
# Unit tests (fast, no external deps)
|
||||
go test ./internal/... -short -count=1
|
||||
|
||||
# Integration tests (requires Docker for testcontainers)
|
||||
go test ./internal/repository/... -count=1 -timeout 120s
|
||||
|
||||
# All tests with coverage
|
||||
go test ./... -coverprofile=coverage.out -count=1
|
||||
go tool cover -func=coverage.out # summary
|
||||
go tool cover -html=coverage.out # browser
|
||||
|
||||
# Race detector
|
||||
go test ./... -race -count=1
|
||||
```
|
||||
|
||||
## ECC 工作流
|
||||
|
||||
```bash
|
||||
# Planning
|
||||
/plan "Add rate limiting to user endpoints"
|
||||
|
||||
# Development
|
||||
/go-test # TDD with Go-specific patterns
|
||||
|
||||
# Review
|
||||
/go-review # Go idioms, error handling, concurrency
|
||||
/security-scan # Secrets and vulnerabilities
|
||||
|
||||
# Before merge
|
||||
go vet ./...
|
||||
staticcheck ./...
|
||||
```
|
||||
|
||||
## Git 工作流
|
||||
|
||||
* `feat:` 新功能,`fix:` 错误修复,`refactor:` 代码更改
|
||||
* 从 `main` 创建功能分支,需要 PR
|
||||
* CI: `go vet`, `staticcheck`, `go test -race`, `golangci-lint`
|
||||
* 部署: 在 CI 中构建 Docker 镜像,部署到 Kubernetes
|
||||
285
docs/zh-CN/examples/rust-api-CLAUDE.md
Normal file
285
docs/zh-CN/examples/rust-api-CLAUDE.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Rust API 服务 — 项目 CLAUDE.md
|
||||
|
||||
> 使用 Axum、PostgreSQL 和 Docker 构建 Rust API 服务的真实示例。
|
||||
> 将此文件复制到您的项目根目录,并根据您的服务进行自定义。
|
||||
|
||||
## 项目概述
|
||||
|
||||
**技术栈:** Rust 1.78+, Axum (Web 框架), SQLx (异步数据库), PostgreSQL, Tokio (异步运行时), Docker
|
||||
|
||||
**架构:** 采用分层架构,包含 handler → service → repository 分离。Axum 用于 HTTP,SQLx 用于编译时类型检查的 SQL,Tower 中间件用于横切关注点。
|
||||
|
||||
## 关键规则
|
||||
|
||||
### Rust 约定
|
||||
|
||||
* 库错误使用 `thiserror`,仅在二进制 crate 或测试中使用 `anyhow`
|
||||
* 生产代码中不使用 `.unwrap()` 或 `.expect()` — 使用 `?` 传播错误
|
||||
* 函数参数中优先使用 `&str` 而非 `String`;所有权转移时返回 `String`
|
||||
* 使用 `clippy` 和 `#![deny(clippy::all, clippy::pedantic)]` — 修复所有警告
|
||||
* 在所有公共类型上派生 `Debug`;仅在需要时派生 `Clone`、`PartialEq`
|
||||
* 除非有 `// SAFETY:` 注释说明理由,否则不使用 `unsafe` 块
|
||||
|
||||
### 数据库
|
||||
|
||||
* 所有查询使用 SQLx 的 `query!` 或 `query_as!` 宏 — 针对模式进行编译时验证
|
||||
* 在 `migrations/` 中使用 `sqlx migrate` 进行迁移 — 切勿直接修改数据库
|
||||
* 使用 `sqlx::Pool<Postgres>` 作为共享状态 — 切勿为每个请求创建连接
|
||||
* 所有查询使用参数化占位符 (`$1`, `$2`) — 切勿使用字符串格式化
|
||||
|
||||
```rust
|
||||
// BAD: String interpolation (SQL injection risk)
|
||||
let q = format!("SELECT * FROM users WHERE id = '{}'", id);
|
||||
|
||||
// GOOD: Parameterized query, compile-time checked
|
||||
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
|
||||
.fetch_optional(&pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
* 为每个模块使用 `thiserror` 定义一个领域错误枚举
|
||||
* 通过 `IntoResponse` 将错误映射到 HTTP 响应 — 切勿暴露内部细节
|
||||
* 使用 `tracing` 进行结构化日志记录 — 切勿使用 `println!` 或 `eprintln!`
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Resource not found")]
|
||||
NotFound,
|
||||
#[error("Validation failed: {0}")]
|
||||
Validation(String),
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error(transparent)]
|
||||
Internal(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self {
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
Self::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||
Self::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||
Self::Internal(err) => {
|
||||
tracing::error!(?err, "internal error");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into())
|
||||
}
|
||||
};
|
||||
(status, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试
|
||||
|
||||
* 单元测试放在每个源文件内的 `#[cfg(test)]` 模块中
|
||||
* 集成测试放在 `tests/` 目录中,使用真实的 PostgreSQL (Testcontainers 或 Docker)
|
||||
* 使用 `#[sqlx::test]` 进行数据库测试,包含自动迁移和回滚
|
||||
* 使用 `mockall` 或 `wiremock` 模拟外部服务
|
||||
|
||||
### 代码风格
|
||||
|
||||
* 最大行长度:100 个字符(由 rustfmt 强制执行)
|
||||
* 导入分组:`std`、外部 crate、`crate`/`super` — 用空行分隔
|
||||
* 模块:每个模块一个文件,`mod.rs` 仅用于重新导出
|
||||
* 类型:PascalCase,函数/变量:snake\_case,常量:UPPER\_SNAKE\_CASE
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
main.rs # Entrypoint, server setup, graceful shutdown
|
||||
lib.rs # Re-exports for integration tests
|
||||
config.rs # Environment config with envy or figment
|
||||
router.rs # Axum router with all routes
|
||||
middleware/
|
||||
auth.rs # JWT extraction and validation
|
||||
logging.rs # Request/response tracing
|
||||
handlers/
|
||||
mod.rs # Route handlers (thin — delegate to services)
|
||||
users.rs
|
||||
orders.rs
|
||||
services/
|
||||
mod.rs # Business logic
|
||||
users.rs
|
||||
orders.rs
|
||||
repositories/
|
||||
mod.rs # Database access (SQLx queries)
|
||||
users.rs
|
||||
orders.rs
|
||||
domain/
|
||||
mod.rs # Domain types, error enums
|
||||
user.rs
|
||||
order.rs
|
||||
migrations/
|
||||
001_create_users.sql
|
||||
002_create_orders.sql
|
||||
tests/
|
||||
common/mod.rs # Shared test helpers, test server setup
|
||||
api_users.rs # Integration tests for user endpoints
|
||||
api_orders.rs # Integration tests for order endpoints
|
||||
```
|
||||
|
||||
## 关键模式
|
||||
|
||||
### Handler (薄层)
|
||||
|
||||
```rust
|
||||
async fn create_user(
|
||||
State(ctx): State<AppState>,
|
||||
Json(payload): Json<CreateUserRequest>,
|
||||
) -> Result<(StatusCode, Json<UserResponse>), AppError> {
|
||||
let user = ctx.user_service.create(payload).await?;
|
||||
Ok((StatusCode::CREATED, Json(UserResponse::from(user))))
|
||||
}
|
||||
```
|
||||
|
||||
### Service (业务逻辑)
|
||||
|
||||
```rust
|
||||
impl UserService {
|
||||
pub async fn create(&self, req: CreateUserRequest) -> Result<User, AppError> {
|
||||
if self.repo.find_by_email(&req.email).await?.is_some() {
|
||||
return Err(AppError::Validation("Email already registered".into()));
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&req.password)?;
|
||||
let user = self.repo.insert(&req.email, &req.name, &password_hash).await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository (数据访问)
|
||||
|
||||
```rust
|
||||
impl UserRepository {
|
||||
pub async fn find_by_email(&self, email: &str) -> Result<Option<User>, sqlx::Error> {
|
||||
sqlx::query_as!(User, "SELECT * FROM users WHERE email = $1", email)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
&self,
|
||||
email: &str,
|
||||
name: &str,
|
||||
password_hash: &str,
|
||||
) -> Result<User, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
r#"INSERT INTO users (email, name, password_hash)
|
||||
VALUES ($1, $2, $3) RETURNING *"#,
|
||||
email, name, password_hash,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_create_user() {
|
||||
let app = spawn_test_app().await;
|
||||
|
||||
let response = app
|
||||
.client
|
||||
.post(&format!("{}/api/v1/users", app.address))
|
||||
.json(&json!({
|
||||
"email": "alice@example.com",
|
||||
"name": "Alice",
|
||||
"password": "securepassword123"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
let body: serde_json::Value = response.json().await.unwrap();
|
||||
assert_eq!(body["email"], "alice@example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_user_duplicate_email() {
|
||||
let app = spawn_test_app().await;
|
||||
// Create first user
|
||||
create_test_user(&app, "alice@example.com").await;
|
||||
// Attempt duplicate
|
||||
let response = create_user_request(&app, "alice@example.com").await;
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
```bash
|
||||
# Server
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
RUST_LOG=info,tower_http=debug
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
||||
|
||||
# Auth
|
||||
JWT_SECRET=your-secret-key-min-32-chars
|
||||
JWT_EXPIRY_HOURS=24
|
||||
|
||||
# Optional
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
|
||||
# Run specific test module
|
||||
cargo test api_users
|
||||
|
||||
# Check coverage (requires cargo-llvm-cov)
|
||||
cargo llvm-cov --html
|
||||
open target/llvm-cov/html/index.html
|
||||
|
||||
# Lint
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
# Format check
|
||||
cargo fmt -- --check
|
||||
```
|
||||
|
||||
## ECC 工作流
|
||||
|
||||
```bash
|
||||
# Planning
|
||||
/plan "Add order fulfillment with Stripe payment"
|
||||
|
||||
# Development with TDD
|
||||
/tdd # cargo test-based TDD workflow
|
||||
|
||||
# Review
|
||||
/code-review # Rust-specific code review
|
||||
/security-scan # Dependency audit + unsafe scan
|
||||
|
||||
# Verification
|
||||
/verify # Build, clippy, test, security scan
|
||||
```
|
||||
|
||||
## Git 工作流
|
||||
|
||||
* `feat:` 新功能,`fix:` 错误修复,`refactor:` 代码变更
|
||||
* 从 `main` 创建功能分支,需要 PR
|
||||
* CI:`cargo fmt --check`、`cargo clippy`、`cargo test`、`cargo audit`
|
||||
* 部署:使用 `scratch` 或 `distroless` 基础镜像的 Docker 多阶段构建
|
||||
166
docs/zh-CN/examples/saas-nextjs-CLAUDE.md
Normal file
166
docs/zh-CN/examples/saas-nextjs-CLAUDE.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# SaaS 应用程序 — 项目 CLAUDE.md
|
||||
|
||||
> 一个 Next.js + Supabase + Stripe SaaS 应用程序的真实示例。
|
||||
> 将此复制到您的项目根目录,并根据您的技术栈进行自定义。
|
||||
|
||||
## 项目概览
|
||||
|
||||
**技术栈:** Next.js 15(App Router)、TypeScript、Supabase(身份验证 + 数据库)、Stripe(计费)、Tailwind CSS、Playwright(端到端测试)
|
||||
|
||||
**架构:** 默认使用服务器组件。仅在需要交互性时使用客户端组件。API 路由用于 Webhook,服务器操作用于数据变更。
|
||||
|
||||
## 关键规则
|
||||
|
||||
### 数据库
|
||||
|
||||
* 所有查询均使用启用 RLS 的 Supabase 客户端 — 绝不要绕过 RLS
|
||||
* 迁移在 `supabase/migrations/` 中 — 绝不要直接修改数据库
|
||||
* 使用带有明确列列表的 `select()`,而不是 `select('*')`
|
||||
* 所有面向用户的查询必须包含 `.limit()` 以防止返回无限制的结果
|
||||
|
||||
### 身份验证
|
||||
|
||||
* 在服务器组件中使用来自 `@supabase/ssr` 的 `createServerClient()`
|
||||
* 在客户端组件中使用来自 `@supabase/ssr` 的 `createBrowserClient()`
|
||||
* 受保护的路由检查 `getUser()` — 绝不要仅依赖 `getSession()` 进行身份验证
|
||||
* `middleware.ts` 中的中间件会在每个请求上刷新身份验证令牌
|
||||
|
||||
### 计费
|
||||
|
||||
* Stripe webhook 处理程序在 `app/api/webhooks/stripe/route.ts` 中
|
||||
* 绝不要信任客户端的定价数据 — 始终在服务器端从 Stripe 获取
|
||||
* 通过 `subscription_status` 列检查订阅状态,由 webhook 同步
|
||||
* 免费层用户:3 个项目,每天 100 次 API 调用
|
||||
|
||||
### 代码风格
|
||||
|
||||
* 代码或注释中不使用表情符号
|
||||
* 仅使用不可变模式 — 使用展开运算符,永不直接修改
|
||||
* 服务器组件:不使用 `'use client'` 指令,不使用 `useState`/`useEffect`
|
||||
* 客户端组件:`'use client'` 放在顶部,保持最小化 — 将逻辑提取到钩子中
|
||||
* 所有输入验证(API 路由、表单、环境变量)优先使用 Zod 模式
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
app/
|
||||
(auth)/ # Auth pages (login, signup, forgot-password)
|
||||
(dashboard)/ # Protected dashboard pages
|
||||
api/
|
||||
webhooks/ # Stripe, Supabase webhooks
|
||||
layout.tsx # Root layout with providers
|
||||
components/
|
||||
ui/ # Shadcn/ui components
|
||||
forms/ # Form components with validation
|
||||
dashboard/ # Dashboard-specific components
|
||||
hooks/ # Custom React hooks
|
||||
lib/
|
||||
supabase/ # Supabase client factories
|
||||
stripe/ # Stripe client and helpers
|
||||
utils.ts # General utilities
|
||||
types/ # Shared TypeScript types
|
||||
supabase/
|
||||
migrations/ # Database migrations
|
||||
seed.sql # Development seed data
|
||||
```
|
||||
|
||||
## 关键模式
|
||||
|
||||
### API 响应格式
|
||||
|
||||
```typescript
|
||||
type ApiResponse<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: string; code?: string }
|
||||
```
|
||||
|
||||
### 服务器操作模式
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { createServerClient } from '@/lib/supabase/server'
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
})
|
||||
|
||||
export async function createProject(formData: FormData) {
|
||||
const parsed = schema.safeParse({ name: formData.get('name') })
|
||||
if (!parsed.success) {
|
||||
return { success: false, error: parsed.error.flatten() }
|
||||
}
|
||||
|
||||
const supabase = await createServerClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return { success: false, error: 'Unauthorized' }
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('projects')
|
||||
.insert({ name: parsed.data.name, user_id: user.id })
|
||||
.select('id, name, created_at')
|
||||
.single()
|
||||
|
||||
if (error) return { success: false, error: 'Failed to create project' }
|
||||
return { success: true, data }
|
||||
}
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
```bash
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY= # Server-only, never expose to client
|
||||
|
||||
# Stripe
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||
|
||||
# App
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
```bash
|
||||
/tdd # Unit + integration tests for new features
|
||||
/e2e # Playwright tests for auth flow, billing, dashboard
|
||||
/test-coverage # Verify 80%+ coverage
|
||||
```
|
||||
|
||||
### 关键的端到端测试流程
|
||||
|
||||
1. 注册 → 邮箱验证 → 创建第一个项目
|
||||
2. 登录 → 仪表盘 → CRUD 操作
|
||||
3. 升级计划 → Stripe 结账 → 订阅激活
|
||||
4. Webhook:订阅取消 → 降级到免费层
|
||||
|
||||
## ECC 工作流
|
||||
|
||||
```bash
|
||||
# Planning a feature
|
||||
/plan "Add team invitations with email notifications"
|
||||
|
||||
# Developing with TDD
|
||||
/tdd
|
||||
|
||||
# Before committing
|
||||
/code-review
|
||||
/security-scan
|
||||
|
||||
# Before release
|
||||
/e2e
|
||||
/test-coverage
|
||||
```
|
||||
|
||||
## Git 工作流
|
||||
|
||||
* `feat:` 新功能,`fix:` 错误修复,`refactor:` 代码变更
|
||||
* 从 `main` 创建功能分支,需要 PR
|
||||
* CI 运行:代码检查、类型检查、单元测试、端到端测试
|
||||
* 部署:在 PR 上部署到 Vercel 预览环境,在合并到 `main` 时部署到生产环境
|
||||
Reference in New Issue
Block a user