@humanbased/crosscheck 1.2.0-beta.80 → 1.2.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/LICENSE +1 -1
- package/README.md +388 -149
- package/README.zh.md +1 -1
- package/crosscheck.config.example.yml +2 -63
- package/dist/__tests__/codex.test.js +27 -14
- package/dist/__tests__/codex.test.js.map +1 -1
- package/dist/__tests__/comment-bodies.test.js +1 -49
- package/dist/__tests__/comment-bodies.test.js.map +1 -1
- package/dist/__tests__/conflict-resolve.test.js +1 -44
- package/dist/__tests__/conflict-resolve.test.js.map +1 -1
- package/dist/__tests__/fix.test.js +0 -33
- package/dist/__tests__/fix.test.js.map +1 -1
- package/dist/__tests__/onboard-preservation.test.js +0 -5
- package/dist/__tests__/onboard-preservation.test.js.map +1 -1
- package/dist/__tests__/optimize.test.js +0 -2
- package/dist/__tests__/optimize.test.js.map +1 -1
- package/dist/__tests__/repo-picker.test.js +1 -7
- package/dist/__tests__/repo-picker.test.js.map +1 -1
- package/dist/__tests__/review-comment-body.test.js +0 -35
- package/dist/__tests__/review-comment-body.test.js.map +1 -1
- package/dist/__tests__/review-models.test.js +3 -19
- package/dist/__tests__/review-models.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +1 -29
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/cli.js +0 -29
- package/dist/cli.js.map +1 -1
- package/dist/commands/onboard.d.ts +0 -11
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +2 -88
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +6 -65
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +3 -23
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +1 -53
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +6 -52
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/loader.d.ts +1 -3
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +0 -13
- package/dist/config/loader.js.map +1 -1
- package/dist/config/review-model-tiers.json +3 -3
- package/dist/config/schema.d.ts +10 -254
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -62
- package/dist/config/schema.js.map +1 -1
- package/dist/github/client.d.ts +1 -6
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +7 -15
- package/dist/github/client.js.map +1 -1
- package/dist/issues/ticket-ref.d.ts.map +1 -1
- package/dist/issues/ticket-ref.js +5 -6
- package/dist/issues/ticket-ref.js.map +1 -1
- package/dist/lib/clone.d.ts +0 -1
- package/dist/lib/clone.d.ts.map +1 -1
- package/dist/lib/clone.js +10 -32
- package/dist/lib/clone.js.map +1 -1
- package/dist/lib/comment-bodies.d.ts +0 -35
- package/dist/lib/comment-bodies.d.ts.map +1 -1
- package/dist/lib/comment-bodies.js +9 -43
- package/dist/lib/comment-bodies.js.map +1 -1
- package/dist/lib/repo-picker.d.ts +0 -3
- package/dist/lib/repo-picker.d.ts.map +1 -1
- package/dist/lib/repo-picker.js +9 -45
- package/dist/lib/repo-picker.js.map +1 -1
- package/dist/lib/review-models.d.ts +2 -15
- package/dist/lib/review-models.d.ts.map +1 -1
- package/dist/lib/review-models.js +6 -26
- package/dist/lib/review-models.js.map +1 -1
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +42 -186
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +0 -9
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js +0 -20
- package/dist/lib/workflow.js.map +1 -1
- package/dist/reviewers/claude.d.ts +1 -4
- package/dist/reviewers/claude.d.ts.map +1 -1
- package/dist/reviewers/claude.js +7 -39
- package/dist/reviewers/claude.js.map +1 -1
- package/dist/reviewers/codex.d.ts +1 -3
- package/dist/reviewers/codex.d.ts.map +1 -1
- package/dist/reviewers/codex.js +70 -76
- package/dist/reviewers/codex.js.map +1 -1
- package/dist/reviewers/conflict-resolve.d.ts +1 -3
- package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
- package/dist/reviewers/conflict-resolve.js +6 -21
- package/dist/reviewers/conflict-resolve.js.map +1 -1
- package/dist/reviewers/fix.d.ts +2 -5
- package/dist/reviewers/fix.d.ts.map +1 -1
- package/dist/reviewers/fix.js +10 -26
- package/dist/reviewers/fix.js.map +1 -1
- package/get-started.md +4 -139
- package/package.json +3 -4
- package/assets/icon-256.png +0 -0
- package/assets/linear-comment.svg +0 -18
- package/assets/linear-onboard.svg +0 -30
- package/assets/linear-status.svg +0 -23
- package/assets/linear-test.svg +0 -34
- package/assets/skills/code-review/.crosscheck-skill.json +0 -9
- package/assets/skills/code-review/LICENSE +0 -21
- package/assets/skills/code-review/SKILL.md +0 -89
- package/assets/skills/code-review/agents/openai.yaml +0 -3
- package/assets/skills/code-review-skill/.crosscheck-skill.json +0 -9
- package/assets/skills/code-review-skill/LICENSE +0 -21
- package/assets/skills/code-review-skill/SKILL.md +0 -231
- package/assets/skills/code-review-skill/assets/pr-review-template.md +0 -137
- package/assets/skills/code-review-skill/assets/review-checklist.md +0 -123
- package/assets/skills/code-review-skill/reference/angular.md +0 -768
- package/assets/skills/code-review-skill/reference/architecture-review-guide.md +0 -472
- package/assets/skills/code-review-skill/reference/c.md +0 -890
- package/assets/skills/code-review-skill/reference/code-quality-universal.md +0 -488
- package/assets/skills/code-review-skill/reference/code-review-best-practices.md +0 -136
- package/assets/skills/code-review-skill/reference/common-bugs-checklist.md +0 -286
- package/assets/skills/code-review-skill/reference/cpp.md +0 -893
- package/assets/skills/code-review-skill/reference/cross-cutting/async-concurrency-patterns.md +0 -515
- package/assets/skills/code-review-skill/reference/cross-cutting/error-handling-principles.md +0 -492
- package/assets/skills/code-review-skill/reference/cross-cutting/n-plus-one-queries.md +0 -309
- package/assets/skills/code-review-skill/reference/cross-cutting/sql-injection-prevention.md +0 -308
- package/assets/skills/code-review-skill/reference/cross-cutting/xss-prevention.md +0 -264
- package/assets/skills/code-review-skill/reference/csharp.md +0 -525
- package/assets/skills/code-review-skill/reference/css-less-sass.md +0 -661
- package/assets/skills/code-review-skill/reference/django.md +0 -985
- package/assets/skills/code-review-skill/reference/fastapi.md +0 -580
- package/assets/skills/code-review-skill/reference/go.md +0 -993
- package/assets/skills/code-review-skill/reference/java.md +0 -409
- package/assets/skills/code-review-skill/reference/java8.md +0 -586
- package/assets/skills/code-review-skill/reference/kotlin.md +0 -1018
- package/assets/skills/code-review-skill/reference/nestjs.md +0 -593
- package/assets/skills/code-review-skill/reference/performance-review-guide.md +0 -816
- package/assets/skills/code-review-skill/reference/php.md +0 -684
- package/assets/skills/code-review-skill/reference/python.md +0 -1073
- package/assets/skills/code-review-skill/reference/qt.md +0 -757
- package/assets/skills/code-review-skill/reference/react.md +0 -871
- package/assets/skills/code-review-skill/reference/ruby.md +0 -964
- package/assets/skills/code-review-skill/reference/rust.md +0 -846
- package/assets/skills/code-review-skill/reference/security-review-guide.md +0 -494
- package/assets/skills/code-review-skill/reference/svelte.md +0 -1064
- package/assets/skills/code-review-skill/reference/swift.md +0 -936
- package/assets/skills/code-review-skill/reference/typescript.md +0 -1016
- package/assets/skills/code-review-skill/reference/vue.md +0 -924
- package/assets/skills/code-review-skill/reference/zig.md +0 -440
- package/assets/skills/code-review-skill/scripts/pr-analyzer.py +0 -435
- package/assets/skills/code-review-skill/scripts/test_pr_analyzer.py +0 -380
- package/assets/skills/codebase-design/.crosscheck-skill.json +0 -9
- package/assets/skills/codebase-design/DEEPENING.md +0 -37
- package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/assets/skills/codebase-design/LICENSE +0 -21
- package/assets/skills/codebase-design/SKILL.md +0 -114
- package/assets/skills/codebase-design/agents/openai.yaml +0 -3
- package/assets/skills/diagnosing-bugs/.crosscheck-skill.json +0 -9
- package/assets/skills/diagnosing-bugs/LICENSE +0 -21
- package/assets/skills/diagnosing-bugs/SKILL.md +0 -134
- package/assets/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/assets/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -41
- package/dist/__tests__/can-write-verdict.test.d.ts +0 -2
- package/dist/__tests__/can-write-verdict.test.d.ts.map +0 -1
- package/dist/__tests__/can-write-verdict.test.js +0 -31
- package/dist/__tests__/can-write-verdict.test.js.map +0 -1
- package/dist/__tests__/linear-branding.test.d.ts +0 -2
- package/dist/__tests__/linear-branding.test.d.ts.map +0 -1
- package/dist/__tests__/linear-branding.test.js +0 -156
- package/dist/__tests__/linear-branding.test.js.map +0 -1
- package/dist/__tests__/linear-client.test.d.ts +0 -2
- package/dist/__tests__/linear-client.test.d.ts.map +0 -1
- package/dist/__tests__/linear-client.test.js +0 -120
- package/dist/__tests__/linear-client.test.js.map +0 -1
- package/dist/__tests__/linear-comment.test.d.ts +0 -2
- package/dist/__tests__/linear-comment.test.d.ts.map +0 -1
- package/dist/__tests__/linear-comment.test.js +0 -151
- package/dist/__tests__/linear-comment.test.js.map +0 -1
- package/dist/__tests__/linear-identity.test.d.ts +0 -2
- package/dist/__tests__/linear-identity.test.d.ts.map +0 -1
- package/dist/__tests__/linear-identity.test.js +0 -253
- package/dist/__tests__/linear-identity.test.js.map +0 -1
- package/dist/__tests__/linear-notify.test.d.ts +0 -2
- package/dist/__tests__/linear-notify.test.d.ts.map +0 -1
- package/dist/__tests__/linear-notify.test.js +0 -144
- package/dist/__tests__/linear-notify.test.js.map +0 -1
- package/dist/__tests__/linear-ref.test.d.ts +0 -2
- package/dist/__tests__/linear-ref.test.d.ts.map +0 -1
- package/dist/__tests__/linear-ref.test.js +0 -261
- package/dist/__tests__/linear-ref.test.js.map +0 -1
- package/dist/__tests__/linear-test-ref.test.d.ts +0 -2
- package/dist/__tests__/linear-test-ref.test.d.ts.map +0 -1
- package/dist/__tests__/linear-test-ref.test.js +0 -81
- package/dist/__tests__/linear-test-ref.test.js.map +0 -1
- package/dist/__tests__/linear-verify.test.d.ts +0 -2
- package/dist/__tests__/linear-verify.test.d.ts.map +0 -1
- package/dist/__tests__/linear-verify.test.js +0 -132
- package/dist/__tests__/linear-verify.test.js.map +0 -1
- package/dist/__tests__/linear-worker.test.d.ts +0 -2
- package/dist/__tests__/linear-worker.test.d.ts.map +0 -1
- package/dist/__tests__/linear-worker.test.js +0 -83
- package/dist/__tests__/linear-worker.test.js.map +0 -1
- package/dist/__tests__/linear-write-possible.test.d.ts +0 -2
- package/dist/__tests__/linear-write-possible.test.d.ts.map +0 -1
- package/dist/__tests__/linear-write-possible.test.js +0 -30
- package/dist/__tests__/linear-write-possible.test.js.map +0 -1
- package/dist/__tests__/repository-guidance.test.d.ts +0 -2
- package/dist/__tests__/repository-guidance.test.d.ts.map +0 -1
- package/dist/__tests__/repository-guidance.test.js +0 -107
- package/dist/__tests__/repository-guidance.test.js.map +0 -1
- package/dist/__tests__/review-strategy.test.d.ts +0 -2
- package/dist/__tests__/review-strategy.test.d.ts.map +0 -1
- package/dist/__tests__/review-strategy.test.js +0 -195
- package/dist/__tests__/review-strategy.test.js.map +0 -1
- package/dist/__tests__/skill-attribution.test.d.ts +0 -2
- package/dist/__tests__/skill-attribution.test.d.ts.map +0 -1
- package/dist/__tests__/skill-attribution.test.js +0 -53
- package/dist/__tests__/skill-attribution.test.js.map +0 -1
- package/dist/__tests__/skill-broker.test.d.ts +0 -2
- package/dist/__tests__/skill-broker.test.d.ts.map +0 -1
- package/dist/__tests__/skill-broker.test.js +0 -144
- package/dist/__tests__/skill-broker.test.js.map +0 -1
- package/dist/__tests__/skill-catalog.test.d.ts +0 -2
- package/dist/__tests__/skill-catalog.test.d.ts.map +0 -1
- package/dist/__tests__/skill-catalog.test.js +0 -40
- package/dist/__tests__/skill-catalog.test.js.map +0 -1
- package/dist/__tests__/skill-installer.test.d.ts +0 -2
- package/dist/__tests__/skill-installer.test.d.ts.map +0 -1
- package/dist/__tests__/skill-installer.test.js +0 -96
- package/dist/__tests__/skill-installer.test.js.map +0 -1
- package/dist/__tests__/skills-config.test.d.ts +0 -2
- package/dist/__tests__/skills-config.test.d.ts.map +0 -1
- package/dist/__tests__/skills-config.test.js +0 -12
- package/dist/__tests__/skills-config.test.js.map +0 -1
- package/dist/commands/linear-test.d.ts +0 -18
- package/dist/commands/linear-test.d.ts.map +0 -1
- package/dist/commands/linear-test.js +0 -130
- package/dist/commands/linear-test.js.map +0 -1
- package/dist/commands/skill.d.ts +0 -2
- package/dist/commands/skill.d.ts.map +0 -1
- package/dist/commands/skill.js +0 -16
- package/dist/commands/skill.js.map +0 -1
- package/dist/config/review-strategy.json +0 -202
- package/dist/lib/repository-guidance.d.ts +0 -2
- package/dist/lib/repository-guidance.d.ts.map +0 -1
- package/dist/lib/repository-guidance.js +0 -55
- package/dist/lib/repository-guidance.js.map +0 -1
- package/dist/lib/review-strategy.d.ts +0 -62
- package/dist/lib/review-strategy.d.ts.map +0 -1
- package/dist/lib/review-strategy.js +0 -235
- package/dist/lib/review-strategy.js.map +0 -1
- package/dist/linear/client.d.ts +0 -18
- package/dist/linear/client.d.ts.map +0 -1
- package/dist/linear/client.js +0 -67
- package/dist/linear/client.js.map +0 -1
- package/dist/linear/comment.d.ts +0 -20
- package/dist/linear/comment.d.ts.map +0 -1
- package/dist/linear/comment.js +0 -57
- package/dist/linear/comment.js.map +0 -1
- package/dist/linear/identity.d.ts +0 -59
- package/dist/linear/identity.d.ts.map +0 -1
- package/dist/linear/identity.js +0 -187
- package/dist/linear/identity.js.map +0 -1
- package/dist/linear/notify.d.ts +0 -35
- package/dist/linear/notify.d.ts.map +0 -1
- package/dist/linear/notify.js +0 -76
- package/dist/linear/notify.js.map +0 -1
- package/dist/linear/ref.d.ts +0 -13
- package/dist/linear/ref.d.ts.map +0 -1
- package/dist/linear/ref.js +0 -90
- package/dist/linear/ref.js.map +0 -1
- package/dist/linear/verify.d.ts +0 -26
- package/dist/linear/verify.d.ts.map +0 -1
- package/dist/linear/verify.js +0 -67
- package/dist/linear/verify.js.map +0 -1
- package/dist/skills/attribution.d.ts +0 -4
- package/dist/skills/attribution.d.ts.map +0 -1
- package/dist/skills/attribution.js +0 -14
- package/dist/skills/attribution.js.map +0 -1
- package/dist/skills/broker-server.d.ts +0 -2
- package/dist/skills/broker-server.d.ts.map +0 -1
- package/dist/skills/broker-server.js +0 -17
- package/dist/skills/broker-server.js.map +0 -1
- package/dist/skills/broker.d.ts +0 -42
- package/dist/skills/broker.d.ts.map +0 -1
- package/dist/skills/broker.js +0 -285
- package/dist/skills/broker.js.map +0 -1
- package/dist/skills/catalog.d.ts +0 -28
- package/dist/skills/catalog.d.ts.map +0 -1
- package/dist/skills/catalog.js +0 -104
- package/dist/skills/catalog.js.map +0 -1
- package/dist/skills/installer.d.ts +0 -10
- package/dist/skills/installer.d.ts.map +0 -1
- package/dist/skills/installer.js +0 -138
- package/dist/skills/installer.js.map +0 -1
- package/dist/skills/integrity.d.ts +0 -4
- package/dist/skills/integrity.d.ts.map +0 -1
- package/dist/skills/integrity.js +0 -36
- package/dist/skills/integrity.js.map +0 -1
- package/docs/dynamic-thoroughness.md +0 -286
- package/docs/linear-identity-contract.md +0 -139
- package/docs/linear-identity.md +0 -293
|
@@ -1,985 +0,0 @@
|
|
|
1
|
-
# Django / DRF Code Review Guide
|
|
2
|
-
|
|
3
|
-
> Django / DRF 代码审查指南,覆盖安全审查、N+1 查询优化、Serializer 反模式、ViewSet 最佳实践、异步视图及生产安全配置等核心主题。
|
|
4
|
-
|
|
5
|
-
## 目录
|
|
6
|
-
|
|
7
|
-
- [安全审查](#安全审查)
|
|
8
|
-
- [N+1 查询优化](#n1-查询优化)
|
|
9
|
-
- [Serializer 反模式](#serializer-反模式)
|
|
10
|
-
- [ViewSet 最佳实践](#viewset-最佳实践)
|
|
11
|
-
- [异步视图](#异步视图)
|
|
12
|
-
- [中间件与设置](#中间件与设置)
|
|
13
|
-
- [Review Checklist](#review-checklist)
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 安全审查
|
|
18
|
-
|
|
19
|
-
### XSS 防护
|
|
20
|
-
|
|
21
|
-
Django 模板引擎默认自动转义。审查重点:`mark_safe`、`autoescape off`、`format_html` 的使用。
|
|
22
|
-
|
|
23
|
-
> **跨框架 XSS 防护详见 [XSS Prevention Guide](cross-cutting/xss-prevention.md)**,含 React/Vue/Angular/Svelte 示例及 CSP 配置。
|
|
24
|
-
|
|
25
|
-
### CSRF 防护
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
from django.views.decorators.csrf import csrf_exempt
|
|
29
|
-
|
|
30
|
-
# ❌ 禁用 CSRF 保护
|
|
31
|
-
@csrf_exempt
|
|
32
|
-
def process_payment(request):
|
|
33
|
-
# 任何恶意网站都可以提交表单
|
|
34
|
-
amount = request.POST["amount"]
|
|
35
|
-
charge(amount)
|
|
36
|
-
|
|
37
|
-
# ✅ 保留默认 CSRF 保护
|
|
38
|
-
from django.middleware.csrf import CsrfViewMiddleware
|
|
39
|
-
|
|
40
|
-
# settings.py — 确保 CSRF 中间件已启用
|
|
41
|
-
MIDDLEWARE = [
|
|
42
|
-
# ...
|
|
43
|
-
"django.middleware.csrf.CsrfViewMiddleware",
|
|
44
|
-
# ...
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
# ✅ API 使用 token 认证代替 CSRF
|
|
48
|
-
# settings.py
|
|
49
|
-
REST_FRAMEWORK = {
|
|
50
|
-
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
51
|
-
"rest_framework.authentication.SessionAuthentication",
|
|
52
|
-
"rest_framework.authentication.TokenAuthentication",
|
|
53
|
-
],
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# ✅ 前端 AJAX 请求带上 CSRF token
|
|
57
|
-
# JavaScript: fetch("/api/endpoint/", {
|
|
58
|
-
# headers: {"X-CSRFToken": document.querySelector("[name=csrfmiddlewaretoken]").value}
|
|
59
|
-
# })
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Cookie 安全设置
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
# settings.py
|
|
66
|
-
|
|
67
|
-
# ❌ 不安全的 cookie 配置
|
|
68
|
-
SESSION_COOKIE_SECURE = False
|
|
69
|
-
CSRF_COOKIE_SECURE = False
|
|
70
|
-
SESSION_COOKIE_HTTPONLY = False
|
|
71
|
-
|
|
72
|
-
# ✅ 生产环境 cookie 安全配置
|
|
73
|
-
SESSION_COOKIE_SECURE = True # HTTPS only
|
|
74
|
-
SESSION_COOKIE_HTTPONLY = True # JavaScript 无法读取
|
|
75
|
-
SESSION_COOKIE_SAMESITE = "Lax" # 防止 CSRF
|
|
76
|
-
CSRF_COOKIE_SECURE = True
|
|
77
|
-
CSRF_COOKIE_HTTPONLY = True
|
|
78
|
-
CSRF_COOKIE_SAMESITE = "Lax"
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### SQL 注入防护
|
|
82
|
-
|
|
83
|
-
Django ORM 自动参数化查询。审查重点:`raw()`、`extra()`、`RawSQL`、`connection.cursor()` 中的字符串拼接。
|
|
84
|
-
|
|
85
|
-
> **跨语言 SQL 注入防护详见 [SQL Injection Prevention Guide](cross-cutting/sql-injection-prevention.md)**,含 Python/Java/Go/Node.js/PHP/C# 示例及 ORM 不安全用法。
|
|
86
|
-
|
|
87
|
-
### 文件上传安全
|
|
88
|
-
|
|
89
|
-
```python
|
|
90
|
-
# settings.py
|
|
91
|
-
|
|
92
|
-
# ❌ 默认上传配置不安全
|
|
93
|
-
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB — 可以接受
|
|
94
|
-
MEDIA_ROOT = "/var/www/uploads" # web 根目录下
|
|
95
|
-
ALLOWED_UPLOAD_TYPES = None # 没有类型限制
|
|
96
|
-
|
|
97
|
-
# ✅ 限制上传大小和位置
|
|
98
|
-
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB
|
|
99
|
-
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB in-memory
|
|
100
|
-
MEDIA_ROOT = "/srv/media/" # web 根目录之外
|
|
101
|
-
|
|
102
|
-
# ✅ 验证文件类型
|
|
103
|
-
import mimetypes
|
|
104
|
-
from pathlib import Path
|
|
105
|
-
|
|
106
|
-
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".pdf"}
|
|
107
|
-
|
|
108
|
-
def validate_upload(file):
|
|
109
|
-
ext = Path(file.name).suffix.lower()
|
|
110
|
-
if ext not in ALLOWED_EXTENSIONS:
|
|
111
|
-
raise ValidationError(f"File type {ext} is not allowed.")
|
|
112
|
-
mime, _ = mimetypes.guess_type(file.name)
|
|
113
|
-
if mime not in {"image/jpeg", "image/png", "application/pdf"}:
|
|
114
|
-
raise ValidationError("Invalid MIME type.")
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## N+1 查询优化
|
|
120
|
-
|
|
121
|
-
> 📖 通用原理和跨语言方案详见 [N+1 查询跨语言指南](cross-cutting/n-plus-one-queries.md)
|
|
122
|
-
|
|
123
|
-
### select_related(ForeignKey / OneToOne)
|
|
124
|
-
|
|
125
|
-
```python
|
|
126
|
-
# ❌ N+1: 每本书查一次出版社
|
|
127
|
-
books = Book.objects.all()
|
|
128
|
-
for book in books:
|
|
129
|
-
print(book.publisher.name) # 额外 N 条查询
|
|
130
|
-
|
|
131
|
-
# ✅ select_related 一次 JOIN 查询
|
|
132
|
-
books = Book.objects.select_related("publisher")
|
|
133
|
-
for book in books:
|
|
134
|
-
print(book.publisher.name) # 无额外查询
|
|
135
|
-
|
|
136
|
-
# ✅ 多层关系
|
|
137
|
-
books = Book.objects.select_related("publisher", "publisher__country")
|
|
138
|
-
|
|
139
|
-
# ✅ 只查需要的字段(延迟加载优化)
|
|
140
|
-
books = Book.objects.select_related("publisher").only(
|
|
141
|
-
"title", "publisher__name"
|
|
142
|
-
)
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### prefetch_related(M2M / 反向 ForeignKey)
|
|
146
|
-
|
|
147
|
-
```python
|
|
148
|
-
# ❌ N+1: 每个作者查一次书
|
|
149
|
-
authors = Author.objects.all()
|
|
150
|
-
for author in authors:
|
|
151
|
-
print(author.books.all()) # 额外 N 条查询
|
|
152
|
-
|
|
153
|
-
# ✅ prefetch_related 两条查询 + Python 合并
|
|
154
|
-
authors = Author.objects.prefetch_related("books")
|
|
155
|
-
for author in authors:
|
|
156
|
-
print(list(author.books.all())) # 无额外查询
|
|
157
|
-
|
|
158
|
-
# ✅ 嵌套 prefetch
|
|
159
|
-
authors = Author.objects.prefetch_related(
|
|
160
|
-
"books",
|
|
161
|
-
"books__publisher",
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# ✅ Prefetch 对象控制预查行为
|
|
165
|
-
from django.db.models import Prefetch
|
|
166
|
-
|
|
167
|
-
authors = Author.objects.prefetch_related(
|
|
168
|
-
Prefetch(
|
|
169
|
-
"books",
|
|
170
|
-
queryset=Book.objects.filter(published=True).only("title", "author_id"),
|
|
171
|
-
to_attr="published_books",
|
|
172
|
-
)
|
|
173
|
-
)
|
|
174
|
-
for author in authors:
|
|
175
|
-
print(author.published_books) # 已过滤,存在 to_attr 中
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### QuerySet 缓存误用
|
|
179
|
-
|
|
180
|
-
```python
|
|
181
|
-
# ❌ count() 后再迭代 —— 两次查询
|
|
182
|
-
qs = Book.objects.all()
|
|
183
|
-
count = qs.count() # 查询 1: SELECT COUNT(*) — 不填充缓存
|
|
184
|
-
titles = [b.title for b in qs] # 查询 2: SELECT * — 重新评估
|
|
185
|
-
|
|
186
|
-
# ✅ 既要对象又要数量时,用 len() 触发一次评估并复用缓存
|
|
187
|
-
qs = Book.objects.all()
|
|
188
|
-
count = len(qs) # 查询 1: SELECT * — 全部加载并缓存
|
|
189
|
-
titles = [b.title for b in qs] # 复用缓存,无新查询
|
|
190
|
-
|
|
191
|
-
# ✅ 如果需要多次迭代,先转 list
|
|
192
|
-
books = list(Book.objects.all()) # 一次查询
|
|
193
|
-
count = len(books)
|
|
194
|
-
titles = [b.title for b in books]
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### 切片/索引不填充缓存
|
|
198
|
-
|
|
199
|
-
```python
|
|
200
|
-
# ❌ 反复索引未评估的 QuerySet —— 每次都查库
|
|
201
|
-
qs = Book.objects.all()
|
|
202
|
-
qs[0] # 查询 1: SELECT ... LIMIT 1
|
|
203
|
-
qs[0] # 查询 2 — 切片/索引不会填充缓存
|
|
204
|
-
|
|
205
|
-
# ✅ 先整体评估,缓存保存所有行,之后索引走缓存
|
|
206
|
-
qs = Book.objects.all()
|
|
207
|
-
list(qs) # SELECT * — 评估并缓存全部行
|
|
208
|
-
qs[0] # 走缓存,无查询
|
|
209
|
-
qs[5] # 走缓存,无查询
|
|
210
|
-
|
|
211
|
-
# ✅ 只需要前 N 条时,切一次并转 list
|
|
212
|
-
books = list(Book.objects.all()[:10]) # 一次查询:SELECT ... LIMIT 10
|
|
213
|
-
first = books[0]
|
|
214
|
-
rest = books[1:] # 已是 Python list,无查询
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### len() vs count()
|
|
218
|
-
|
|
219
|
-
```python
|
|
220
|
-
# ❌ len() 加载全部对象到内存
|
|
221
|
-
total = len(Book.objects.all()) # SELECT * FROM book — 全表加载
|
|
222
|
-
|
|
223
|
-
# ✅ count() 在数据库端计数
|
|
224
|
-
total = Book.objects.count() # SELECT COUNT(*) — 高效
|
|
225
|
-
|
|
226
|
-
# ✅ 如果已经需要 QuerySet 结果,再用 len
|
|
227
|
-
books = list(Book.objects.filter(published=True))
|
|
228
|
-
total = len(books) # 已在内存中,不需要额外查询
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### if qs vs qs.exists()
|
|
232
|
-
|
|
233
|
-
```python
|
|
234
|
-
# ❌ if qs 加载全部记录
|
|
235
|
-
qs = Book.objects.filter(author_id=author_id)
|
|
236
|
-
if qs: # SELECT * FROM book WHERE ... — 全部加载
|
|
237
|
-
return qs[0]
|
|
238
|
-
|
|
239
|
-
# ✅ exists() 只检查是否有记录
|
|
240
|
-
if Book.objects.filter(author_id=author_id).exists():
|
|
241
|
-
return Book.objects.filter(author_id=author_id).first()
|
|
242
|
-
|
|
243
|
-
# ✅ 或者直接 get/first 判空
|
|
244
|
-
book = Book.objects.filter(author_id=author_id).first()
|
|
245
|
-
if book is not None:
|
|
246
|
-
return book
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Serializer 反模式
|
|
252
|
-
|
|
253
|
-
### 排除敏感字段
|
|
254
|
-
|
|
255
|
-
```python
|
|
256
|
-
from rest_framework import serializers
|
|
257
|
-
|
|
258
|
-
# ❌ __all__ 暴露所有字段,包括敏感数据
|
|
259
|
-
class UserSerializer(serializers.ModelSerializer):
|
|
260
|
-
class Meta:
|
|
261
|
-
model = User
|
|
262
|
-
fields = "__all__" # 密码 hash、is_superuser 等全部暴露
|
|
263
|
-
|
|
264
|
-
# ✅ 显式列出允许的字段
|
|
265
|
-
class UserSerializer(serializers.ModelSerializer):
|
|
266
|
-
class Meta:
|
|
267
|
-
model = User
|
|
268
|
-
fields = ["id", "username", "email", "first_name", "last_name"]
|
|
269
|
-
|
|
270
|
-
# ✅ 使用 exclude 时也要注意
|
|
271
|
-
class UserProfileSerializer(serializers.ModelSerializer):
|
|
272
|
-
class Meta:
|
|
273
|
-
model = UserProfile
|
|
274
|
-
exclude = ["internal_notes", "admin_flags"]
|
|
275
|
-
|
|
276
|
-
# ✅ 密码字段用 write_only
|
|
277
|
-
class RegistrationSerializer(serializers.ModelSerializer):
|
|
278
|
-
password = serializers.CharField(write_only=True, min_length=8)
|
|
279
|
-
|
|
280
|
-
class Meta:
|
|
281
|
-
model = User
|
|
282
|
-
fields = ["id", "username", "email", "password"]
|
|
283
|
-
|
|
284
|
-
def create(self, validated_data):
|
|
285
|
-
user = User(**validated_data)
|
|
286
|
-
user.set_password(validated_data["password"])
|
|
287
|
-
user.save()
|
|
288
|
-
return user
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### 缺少验证
|
|
292
|
-
|
|
293
|
-
```python
|
|
294
|
-
from rest_framework import serializers
|
|
295
|
-
|
|
296
|
-
# ❌ 没有验证,信任所有输入
|
|
297
|
-
class OrderSerializer(serializers.ModelSerializer):
|
|
298
|
-
class Meta:
|
|
299
|
-
model = Order
|
|
300
|
-
fields = ["quantity", "price", "discount"]
|
|
301
|
-
|
|
302
|
-
# ✅ 字段级验证
|
|
303
|
-
class OrderSerializer(serializers.ModelSerializer):
|
|
304
|
-
quantity = serializers.IntegerField(min_value=1, max_value=100)
|
|
305
|
-
price = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0)
|
|
306
|
-
discount = serializers.DecimalField(
|
|
307
|
-
max_digits=5, decimal_places=2, min_value=0, max_value=1, required=False
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
class Meta:
|
|
311
|
-
model = Order
|
|
312
|
-
fields = ["quantity", "price", "discount"]
|
|
313
|
-
|
|
314
|
-
# ✅ 对象级验证
|
|
315
|
-
class OrderSerializer(serializers.ModelSerializer):
|
|
316
|
-
class Meta:
|
|
317
|
-
model = Order
|
|
318
|
-
fields = ["quantity", "price", "discount"]
|
|
319
|
-
|
|
320
|
-
def validate(self, attrs):
|
|
321
|
-
if attrs.get("discount", 0) > 0.5 and attrs.get("quantity", 0) < 10:
|
|
322
|
-
raise serializers.ValidationError(
|
|
323
|
-
"Bulk discount requires minimum 10 items."
|
|
324
|
-
)
|
|
325
|
-
return attrs
|
|
326
|
-
|
|
327
|
-
# ✅ 自定义字段验证方法
|
|
328
|
-
class BookingSerializer(serializers.ModelSerializer):
|
|
329
|
-
class Meta:
|
|
330
|
-
model = Booking
|
|
331
|
-
fields = ["start_date", "end_date", "room"]
|
|
332
|
-
|
|
333
|
-
def validate_start_date(self, value):
|
|
334
|
-
if value < date.today():
|
|
335
|
-
raise serializers.ValidationError("Start date cannot be in the past.")
|
|
336
|
-
return value
|
|
337
|
-
|
|
338
|
-
def validate(self, attrs):
|
|
339
|
-
if attrs["end_date"] <= attrs["start_date"]:
|
|
340
|
-
raise serializers.ValidationError("End date must be after start date.")
|
|
341
|
-
return attrs
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### 嵌套写入
|
|
345
|
-
|
|
346
|
-
```python
|
|
347
|
-
from rest_framework import serializers
|
|
348
|
-
|
|
349
|
-
# ❌ 嵌套 Serializer 只读但没有实现 create/update
|
|
350
|
-
class TagSerializer(serializers.ModelSerializer):
|
|
351
|
-
class Meta:
|
|
352
|
-
model = Tag
|
|
353
|
-
fields = ["id", "name"]
|
|
354
|
-
|
|
355
|
-
class ArticleSerializer(serializers.ModelSerializer):
|
|
356
|
-
tags = TagSerializer(many=True) # 嵌套写入会失败
|
|
357
|
-
|
|
358
|
-
class Meta:
|
|
359
|
-
model = Article
|
|
360
|
-
fields = ["id", "title", "tags"]
|
|
361
|
-
|
|
362
|
-
# ✅ 方案 1: 嵌套只读 + PrimaryKeyRelatedField 写入
|
|
363
|
-
class ArticleSerializer(serializers.ModelSerializer):
|
|
364
|
-
tags = TagSerializer(many=True, read_only=True)
|
|
365
|
-
tag_ids = serializers.PrimaryKeyRelatedField(
|
|
366
|
-
queryset=Tag.objects.all(),
|
|
367
|
-
many=True,
|
|
368
|
-
write_only=True,
|
|
369
|
-
source="tags",
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
class Meta:
|
|
373
|
-
model = Article
|
|
374
|
-
fields = ["id", "title", "tags", "tag_ids"]
|
|
375
|
-
|
|
376
|
-
# ✅ 方案 2: 实现 create() 处理嵌套
|
|
377
|
-
class ArticleSerializer(serializers.ModelSerializer):
|
|
378
|
-
tags = TagSerializer(many=True)
|
|
379
|
-
|
|
380
|
-
class Meta:
|
|
381
|
-
model = Article
|
|
382
|
-
fields = ["id", "title", "tags"]
|
|
383
|
-
|
|
384
|
-
def create(self, validated_data):
|
|
385
|
-
tags_data = validated_data.pop("tags")
|
|
386
|
-
article = Article.objects.create(**validated_data)
|
|
387
|
-
for tag_data in tags_data:
|
|
388
|
-
tag, _ = Tag.objects.get_or_create(**tag_data)
|
|
389
|
-
article.tags.add(tag)
|
|
390
|
-
return article
|
|
391
|
-
|
|
392
|
-
def update(self, instance, validated_data):
|
|
393
|
-
tags_data = validated_data.pop("tags", None)
|
|
394
|
-
instance = super().update(instance, validated_data)
|
|
395
|
-
if tags_data is not None:
|
|
396
|
-
instance.tags.clear()
|
|
397
|
-
for tag_data in tags_data:
|
|
398
|
-
tag, _ = Tag.objects.get_or_create(**tag_data)
|
|
399
|
-
instance.tags.add(tag)
|
|
400
|
-
return instance
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### read_only_fields 遗漏
|
|
404
|
-
|
|
405
|
-
```python
|
|
406
|
-
from rest_framework import serializers
|
|
407
|
-
|
|
408
|
-
# ❌ 计算字段和自动字段可被用户覆盖
|
|
409
|
-
class CommentSerializer(serializers.ModelSerializer):
|
|
410
|
-
class Meta:
|
|
411
|
-
model = Comment
|
|
412
|
-
fields = ["id", "body", "author", "created_at", "updated_at"]
|
|
413
|
-
# created_at, updated_at, author 可被客户端篡改
|
|
414
|
-
|
|
415
|
-
# ✅ 标记只读字段
|
|
416
|
-
class CommentSerializer(serializers.ModelSerializer):
|
|
417
|
-
class Meta:
|
|
418
|
-
model = Comment
|
|
419
|
-
fields = ["id", "body", "author", "created_at", "updated_at"]
|
|
420
|
-
read_only_fields = ["author", "created_at", "updated_at"]
|
|
421
|
-
|
|
422
|
-
# ✅ 在视图中设置只读字段(如当前用户)
|
|
423
|
-
class CommentViewSet(viewsets.ModelViewSet):
|
|
424
|
-
serializer_class = CommentSerializer
|
|
425
|
-
|
|
426
|
-
def get_serializer_context(self):
|
|
427
|
-
context = super().get_serializer_context()
|
|
428
|
-
context["request"] = self.request
|
|
429
|
-
return context
|
|
430
|
-
|
|
431
|
-
def perform_create(self, serializer):
|
|
432
|
-
serializer.save(author=self.request.user)
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## ViewSet 最佳实践
|
|
438
|
-
|
|
439
|
-
### 选择正确的基类
|
|
440
|
-
|
|
441
|
-
```python
|
|
442
|
-
from rest_framework import viewsets
|
|
443
|
-
|
|
444
|
-
# ❌ ModelViewSet 提供完整 CRUD,但只需要读取
|
|
445
|
-
class TagViewSet(viewsets.ModelViewSet):
|
|
446
|
-
queryset = Tag.objects.all()
|
|
447
|
-
serializer_class = TagSerializer
|
|
448
|
-
# 暴露了 destroy, update, create — 标签不应被随意修改
|
|
449
|
-
|
|
450
|
-
# ✅ 只读场景用 ReadOnlyModelViewSet
|
|
451
|
-
class TagViewSet(viewsets.ReadOnlyModelViewSet):
|
|
452
|
-
queryset = Tag.objects.all()
|
|
453
|
-
serializer_class = TagSerializer
|
|
454
|
-
# 只提供 list 和 retrieve
|
|
455
|
-
|
|
456
|
-
# ✅ 需要自定义操作时用 Mixin
|
|
457
|
-
from rest_framework import mixins
|
|
458
|
-
|
|
459
|
-
class TagViewSet(
|
|
460
|
-
mixins.ListModelMixin,
|
|
461
|
-
mixins.RetrieveModelMixin,
|
|
462
|
-
mixins.CreateModelMixin,
|
|
463
|
-
generics.GenericAPIView,
|
|
464
|
-
):
|
|
465
|
-
queryset = Tag.objects.all()
|
|
466
|
-
serializer_class = TagSerializer
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### 用户级数据范围限定
|
|
470
|
-
|
|
471
|
-
```python
|
|
472
|
-
from rest_framework import viewsets
|
|
473
|
-
|
|
474
|
-
# ❌ 任何用户可以看到所有数据
|
|
475
|
-
class DocumentViewSet(viewsets.ModelViewSet):
|
|
476
|
-
queryset = Document.objects.all()
|
|
477
|
-
serializer_class = DocumentSerializer
|
|
478
|
-
|
|
479
|
-
# ✅ get_queryset 限定当前用户数据
|
|
480
|
-
class DocumentViewSet(viewsets.ModelViewSet):
|
|
481
|
-
serializer_class = DocumentSerializer
|
|
482
|
-
|
|
483
|
-
def get_queryset(self):
|
|
484
|
-
return Document.objects.filter(
|
|
485
|
-
owner=self.request.user
|
|
486
|
-
).select_related("owner")
|
|
487
|
-
|
|
488
|
-
# ✅ 管理员看全部,普通用户看自己的
|
|
489
|
-
class DocumentViewSet(viewsets.ModelViewSet):
|
|
490
|
-
serializer_class = DocumentSerializer
|
|
491
|
-
|
|
492
|
-
def get_queryset(self):
|
|
493
|
-
qs = Document.objects.select_related("owner")
|
|
494
|
-
if self.request.user.is_staff:
|
|
495
|
-
return qs
|
|
496
|
-
return qs.filter(owner=self.request.user)
|
|
497
|
-
|
|
498
|
-
# ✅ perform_create 自动关联当前用户
|
|
499
|
-
class DocumentViewSet(viewsets.ModelViewSet):
|
|
500
|
-
serializer_class = DocumentSerializer
|
|
501
|
-
|
|
502
|
-
def get_queryset(self):
|
|
503
|
-
return Document.objects.filter(owner=self.request.user)
|
|
504
|
-
|
|
505
|
-
def perform_create(self, serializer):
|
|
506
|
-
serializer.save(owner=self.request.user)
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
### 权限控制
|
|
510
|
-
|
|
511
|
-
```python
|
|
512
|
-
from rest_framework import permissions, viewsets
|
|
513
|
-
|
|
514
|
-
# ❌ 没有权限控制
|
|
515
|
-
class ArticleViewSet(viewsets.ModelViewSet):
|
|
516
|
-
queryset = Article.objects.all()
|
|
517
|
-
serializer_class = ArticleSerializer
|
|
518
|
-
|
|
519
|
-
# ✅ 类级别权限
|
|
520
|
-
class ArticleViewSet(viewsets.ModelViewSet):
|
|
521
|
-
queryset = Article.objects.all()
|
|
522
|
-
serializer_class = ArticleSerializer
|
|
523
|
-
permission_classes = [permissions.IsAuthenticated]
|
|
524
|
-
|
|
525
|
-
# ✅ 操作级别权限
|
|
526
|
-
from rest_framework.decorators import action
|
|
527
|
-
|
|
528
|
-
class ArticleViewSet(viewsets.ModelViewSet):
|
|
529
|
-
queryset = Article.objects.all()
|
|
530
|
-
serializer_class = ArticleSerializer
|
|
531
|
-
|
|
532
|
-
def get_permissions(self):
|
|
533
|
-
if self.action in ("list", "retrieve"):
|
|
534
|
-
return [permissions.AllowAny()]
|
|
535
|
-
if self.action == "create":
|
|
536
|
-
return [permissions.IsAuthenticated()]
|
|
537
|
-
return [permissions.IsAdminUser()]
|
|
538
|
-
|
|
539
|
-
# ✅ 自定义对象级权限
|
|
540
|
-
class IsOwnerOrReadOnly(permissions.BasePermission):
|
|
541
|
-
def has_object_permission(self, request, view, obj):
|
|
542
|
-
if request.method in permissions.SAFE_METHODS:
|
|
543
|
-
return True
|
|
544
|
-
return obj.owner == request.user
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
### 分页和节流
|
|
548
|
-
|
|
549
|
-
```python
|
|
550
|
-
# settings.py
|
|
551
|
-
|
|
552
|
-
# ❌ 没有分页和节流配置
|
|
553
|
-
REST_FRAMEWORK = {
|
|
554
|
-
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
555
|
-
"rest_framework.authentication.SessionAuthentication",
|
|
556
|
-
],
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
# ✅ 全局分页和节流
|
|
560
|
-
REST_FRAMEWORK = {
|
|
561
|
-
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
562
|
-
"PAGE_SIZE": 20,
|
|
563
|
-
"DEFAULT_THROTTLE_CLASSES": [
|
|
564
|
-
"rest_framework.throttling.AnonRateThrottle",
|
|
565
|
-
"rest_framework.throttling.UserRateThrottle",
|
|
566
|
-
],
|
|
567
|
-
"DEFAULT_THROTTLE_RATES": {
|
|
568
|
-
"anon": "100/hour",
|
|
569
|
-
"user": "1000/hour",
|
|
570
|
-
},
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
# ✅ 自定义分页器
|
|
574
|
-
from rest_framework.pagination import PageNumberPagination
|
|
575
|
-
|
|
576
|
-
class StandardPagination(PageNumberPagination):
|
|
577
|
-
page_size = 25
|
|
578
|
-
page_size_query_param = "page_size"
|
|
579
|
-
max_page_size = 100
|
|
580
|
-
|
|
581
|
-
class ArticleViewSet(viewsets.ModelViewSet):
|
|
582
|
-
queryset = Article.objects.all()
|
|
583
|
-
serializer_class = ArticleSerializer
|
|
584
|
-
pagination_class = StandardPagination
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
---
|
|
588
|
-
|
|
589
|
-
## 异步视图
|
|
590
|
-
|
|
591
|
-
### 同步 ORM 在异步视图中的正确使用
|
|
592
|
-
|
|
593
|
-
```python
|
|
594
|
-
import asyncio
|
|
595
|
-
from asgiref.sync import sync_to_async
|
|
596
|
-
from django.http import JsonResponse
|
|
597
|
-
|
|
598
|
-
# ❌ 在 async 视图中直接调用同步 ORM — 阻塞事件循环
|
|
599
|
-
async def user_list(request):
|
|
600
|
-
users = User.objects.all() # Synchronous ORM call in async context!
|
|
601
|
-
data = [{"id": u.id, "name": u.username} for u in users]
|
|
602
|
-
return JsonResponse(data, safe=False)
|
|
603
|
-
|
|
604
|
-
# ✅ 使用 async ORM(Django 4.1+)
|
|
605
|
-
async def user_list(request):
|
|
606
|
-
users = User.objects.all()
|
|
607
|
-
data = []
|
|
608
|
-
async for user in users: # async iteration
|
|
609
|
-
data.append({"id": user.id, "name": user.username})
|
|
610
|
-
return JsonResponse(data, safe=False)
|
|
611
|
-
|
|
612
|
-
# ✅ 使用 aget / afilter / acreate
|
|
613
|
-
async def user_detail(request, pk):
|
|
614
|
-
user = await User.objects.aget(pk=pk)
|
|
615
|
-
return JsonResponse({"id": user.id, "name": user.username})
|
|
616
|
-
|
|
617
|
-
# ✅ 复杂查询用 sync_to_async
|
|
618
|
-
@sync_to_async
|
|
619
|
-
def get_user_with_profile(pk):
|
|
620
|
-
return User.objects.select_related("profile").get(pk=pk)
|
|
621
|
-
|
|
622
|
-
async def user_profile(request, pk):
|
|
623
|
-
user = await get_user_with_profile(pk)
|
|
624
|
-
return JsonResponse({
|
|
625
|
-
"id": user.id,
|
|
626
|
-
"name": user.username,
|
|
627
|
-
"bio": user.profile.bio,
|
|
628
|
-
})
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
### 遗漏 await
|
|
632
|
-
|
|
633
|
-
```python
|
|
634
|
-
from django.http import JsonResponse
|
|
635
|
-
|
|
636
|
-
# ❌ 忘记 await — coroutine 不会执行,返回协程对象而非数据
|
|
637
|
-
async def user_detail(request, pk):
|
|
638
|
-
user = User.objects.aget(pk=pk) # Missing await!
|
|
639
|
-
# user 是一个 coroutine 对象,不是 User 实例
|
|
640
|
-
return JsonResponse({"name": user.username}) # RuntimeError
|
|
641
|
-
|
|
642
|
-
# ✅ 始终 await 异步 ORM 调用
|
|
643
|
-
async def user_detail(request, pk):
|
|
644
|
-
user = await User.objects.aget(pk=pk)
|
|
645
|
-
return JsonResponse({"name": user.username})
|
|
646
|
-
|
|
647
|
-
# ✅ 使用aget_or_404 的异步版本
|
|
648
|
-
from django.shortcuts import aget_object_or_404
|
|
649
|
-
|
|
650
|
-
async def user_detail(request, pk):
|
|
651
|
-
user = await aget_object_or_404(User, pk=pk)
|
|
652
|
-
return JsonResponse({"name": user.username})
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
### 异步视图中的事务
|
|
656
|
-
|
|
657
|
-
```python
|
|
658
|
-
from django.db import transaction
|
|
659
|
-
from asgiref.sync import sync_to_async
|
|
660
|
-
|
|
661
|
-
# ❌ transaction.atomic() 是同步的,不能直接在 async 中用
|
|
662
|
-
async def create_order(request):
|
|
663
|
-
async with transaction.atomic(): # Error! Not async-compatible
|
|
664
|
-
order = await Order.objects.acreate(total=100)
|
|
665
|
-
await OrderItem.objects.acreate(order=order, product_id=1)
|
|
666
|
-
return JsonResponse({"order_id": order.id})
|
|
667
|
-
|
|
668
|
-
# ✅ 用 sync_to_async 包装事务块
|
|
669
|
-
@sync_to_async
|
|
670
|
-
def _create_order_with_items():
|
|
671
|
-
with transaction.atomic():
|
|
672
|
-
order = Order.objects.create(total=100)
|
|
673
|
-
OrderItem.objects.create(order=order, product_id=1)
|
|
674
|
-
return order.id
|
|
675
|
-
|
|
676
|
-
async def create_order(request):
|
|
677
|
-
order_id = await _create_order_with_items()
|
|
678
|
-
return JsonResponse({"order_id": order_id})
|
|
679
|
-
|
|
680
|
-
# ✅ 多个操作打包到一个 sync_to_async 中
|
|
681
|
-
@sync_to_async
|
|
682
|
-
def _bulk_create_products(items):
|
|
683
|
-
with transaction.atomic():
|
|
684
|
-
products = Product.objects.bulk_create([Product(**i) for i in items])
|
|
685
|
-
return [p.id for p in products]
|
|
686
|
-
|
|
687
|
-
async def import_products(request):
|
|
688
|
-
ids = await _bulk_create_products(request.data)
|
|
689
|
-
return JsonResponse({"ids": ids})
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
### 同步中间件拖慢异步性能
|
|
693
|
-
|
|
694
|
-
```python
|
|
695
|
-
# ❌ 同步中间件会把 async 视图降级为同步执行
|
|
696
|
-
class TimingMiddleware:
|
|
697
|
-
def __init__(self, get_response):
|
|
698
|
-
self.get_response = get_response
|
|
699
|
-
|
|
700
|
-
def __call__(self, request): # sync — blocks async views
|
|
701
|
-
start = time.time()
|
|
702
|
-
response = self.get_response(request)
|
|
703
|
-
elapsed = time.time() - start
|
|
704
|
-
response["X-Elapsed"] = str(elapsed)
|
|
705
|
-
return response
|
|
706
|
-
|
|
707
|
-
# ✅ async-capable 中间件:async def __call__,并在 __init__ 里标记实例
|
|
708
|
-
import time
|
|
709
|
-
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
|
710
|
-
|
|
711
|
-
class TimingMiddleware:
|
|
712
|
-
async_capable = True
|
|
713
|
-
sync_capable = False
|
|
714
|
-
|
|
715
|
-
def __init__(self, get_response):
|
|
716
|
-
self.get_response = get_response
|
|
717
|
-
# get_response 是协程函数时标记自己,Django 才会 await 这个实例
|
|
718
|
-
if iscoroutinefunction(self.get_response):
|
|
719
|
-
markcoroutinefunction(self)
|
|
720
|
-
|
|
721
|
-
async def __call__(self, request):
|
|
722
|
-
start = time.time()
|
|
723
|
-
response = await self.get_response(request)
|
|
724
|
-
elapsed = time.time() - start
|
|
725
|
-
response["X-Elapsed"] = str(elapsed)
|
|
726
|
-
return response
|
|
727
|
-
|
|
728
|
-
# ✅ 要同时兼容同步和异步,用工厂函数 + 内置装饰器
|
|
729
|
-
from django.utils.decorators import sync_and_async_middleware
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### async for 迭代模式
|
|
733
|
-
|
|
734
|
-
```python
|
|
735
|
-
from django.http import JsonResponse
|
|
736
|
-
|
|
737
|
-
# ❌ 同步迭代大型 QuerySet 在 async 视图中阻塞
|
|
738
|
-
async def export_users(request):
|
|
739
|
-
users = User.objects.all()
|
|
740
|
-
data = [] # 同步迭代阻塞事件循环
|
|
741
|
-
for user in users:
|
|
742
|
-
data.append({"id": user.id, "name": user.username})
|
|
743
|
-
return JsonResponse(data, safe=False)
|
|
744
|
-
|
|
745
|
-
# ✅ 使用 async for 异步迭代
|
|
746
|
-
async def export_users(request):
|
|
747
|
-
data = []
|
|
748
|
-
async for user in User.objects.all():
|
|
749
|
-
data.append({"id": user.id, "name": user.username})
|
|
750
|
-
return JsonResponse(data, safe=False)
|
|
751
|
-
|
|
752
|
-
# ✅ 大数据集使用 aiterator() + 分块处理
|
|
753
|
-
async def export_large_dataset(request):
|
|
754
|
-
data = []
|
|
755
|
-
async for user in User.objects.all().aiterator(chunk_size=500):
|
|
756
|
-
data.append({"id": user.id, "name": user.username})
|
|
757
|
-
return JsonResponse(data, safe=False)
|
|
758
|
-
|
|
759
|
-
# ✅ 使用 values() 减少内存
|
|
760
|
-
async def lightweight_export(request):
|
|
761
|
-
data = []
|
|
762
|
-
async for row in User.objects.values("id", "username"):
|
|
763
|
-
data.append(row)
|
|
764
|
-
return JsonResponse(data, safe=False)
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
---
|
|
768
|
-
|
|
769
|
-
## 中间件与设置
|
|
770
|
-
|
|
771
|
-
### 生产安全配置清单
|
|
772
|
-
|
|
773
|
-
```python
|
|
774
|
-
# settings.py — 生产环境必须的安全设置
|
|
775
|
-
|
|
776
|
-
# ❌ 开发默认值不应出现在生产环境
|
|
777
|
-
DEBUG = True
|
|
778
|
-
SECRET_KEY = "django-insecure-..."
|
|
779
|
-
ALLOWED_HOSTS = ["*"]
|
|
780
|
-
SECURE_SSL_REDIRECT = False
|
|
781
|
-
|
|
782
|
-
# ✅ 生产环境安全配置
|
|
783
|
-
|
|
784
|
-
# --- 基础安全 ---
|
|
785
|
-
DEBUG = False
|
|
786
|
-
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # 从环境变量读取
|
|
787
|
-
ALLOWED_HOSTS = ["example.com", "www.example.com"]
|
|
788
|
-
|
|
789
|
-
# --- HTTPS ---
|
|
790
|
-
SECURE_SSL_REDIRECT = True # HTTP 重定向到 HTTPS
|
|
791
|
-
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
792
|
-
SESSION_COOKIE_SECURE = True
|
|
793
|
-
CSRF_COOKIE_SECURE = True
|
|
794
|
-
|
|
795
|
-
# --- 安全头 ---
|
|
796
|
-
SECURE_HSTS_SECONDS = 31536000 # 1 year HSTS
|
|
797
|
-
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
798
|
-
SECURE_HSTS_PRELOAD = True
|
|
799
|
-
SECURE_CONTENT_TYPE_NOSNIFF = True # X-Content-Type-Options: nosniff
|
|
800
|
-
X_FRAME_OPTIONS = "DENY" # 防止 clickjacking
|
|
801
|
-
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
|
802
|
-
|
|
803
|
-
# --- 密码验证 ---
|
|
804
|
-
AUTH_PASSWORD_VALIDATORS = [
|
|
805
|
-
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
806
|
-
"OPTIONS": {"min_length": 12}},
|
|
807
|
-
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
808
|
-
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
|
809
|
-
]
|
|
810
|
-
|
|
811
|
-
# --- Session ---
|
|
812
|
-
SESSION_COOKIE_AGE = 3600 * 8 # 8 hours
|
|
813
|
-
SESSION_SAVE_EVERY_REQUEST = True
|
|
814
|
-
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
### 数据库连接安全
|
|
818
|
-
|
|
819
|
-
```python
|
|
820
|
-
# settings.py
|
|
821
|
-
|
|
822
|
-
# ❌ 明文密码在代码中
|
|
823
|
-
DATABASES = {
|
|
824
|
-
"default": {
|
|
825
|
-
"ENGINE": "django.db.backends.postgresql",
|
|
826
|
-
"NAME": "mydb",
|
|
827
|
-
"USER": "admin",
|
|
828
|
-
"PASSWORD": "hunter2", # 不要硬编码密码
|
|
829
|
-
"HOST": "localhost",
|
|
830
|
-
"PORT": "5432",
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
# ✅ 从环境变量读取
|
|
835
|
-
DATABASES = {
|
|
836
|
-
"default": {
|
|
837
|
-
"ENGINE": "django.db.backends.postgresql",
|
|
838
|
-
"NAME": os.environ.get("DB_NAME", "mydb"),
|
|
839
|
-
"USER": os.environ.get("DB_USER", "mydb_user"),
|
|
840
|
-
"PASSWORD": os.environ["DB_PASSWORD"],
|
|
841
|
-
"HOST": os.environ.get("DB_HOST", "localhost"),
|
|
842
|
-
"PORT": os.environ.get("DB_PORT", "5432"),
|
|
843
|
-
"OPTIONS": {
|
|
844
|
-
"sslmode": "require", # 强制 SSL 连接
|
|
845
|
-
},
|
|
846
|
-
"CONN_MAX_AGE": 60, # 持久连接
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
### CORS 配置
|
|
852
|
-
|
|
853
|
-
```python
|
|
854
|
-
# settings.py (using django-cors-headers)
|
|
855
|
-
|
|
856
|
-
# ❌ 允许所有来源
|
|
857
|
-
CORS_ALLOW_ALL_ORIGINS = True
|
|
858
|
-
|
|
859
|
-
# ✅ 限制允许的来源
|
|
860
|
-
CORS_ALLOWED_ORIGINS = [
|
|
861
|
-
"https://example.com",
|
|
862
|
-
"https://app.example.com",
|
|
863
|
-
]
|
|
864
|
-
|
|
865
|
-
# ✅ 生产环境 CORS 设置
|
|
866
|
-
CORS_ALLOW_ALL_ORIGINS = False
|
|
867
|
-
CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
|
|
868
|
-
CORS_ALLOW_CREDENTIALS = True
|
|
869
|
-
CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
870
|
-
CORS_ALLOW_HEADERS = [
|
|
871
|
-
"authorization",
|
|
872
|
-
"content-type",
|
|
873
|
-
"x-csrftoken",
|
|
874
|
-
]
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
### 日志配置
|
|
878
|
-
|
|
879
|
-
```python
|
|
880
|
-
# settings.py
|
|
881
|
-
|
|
882
|
-
# ❌ 默认日志配置(或不配置)
|
|
883
|
-
LOGGING = {}
|
|
884
|
-
|
|
885
|
-
# ✅ 生产环境日志配置
|
|
886
|
-
LOGGING = {
|
|
887
|
-
"version": 1,
|
|
888
|
-
"disable_existing_loggers": False,
|
|
889
|
-
"formatters": {
|
|
890
|
-
"verbose": {
|
|
891
|
-
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
|
|
892
|
-
"style": "{",
|
|
893
|
-
},
|
|
894
|
-
},
|
|
895
|
-
"handlers": {
|
|
896
|
-
"file": {
|
|
897
|
-
"level": "INFO",
|
|
898
|
-
"class": "logging.handlers.RotatingFileHandler",
|
|
899
|
-
"filename": "/var/log/django/app.log",
|
|
900
|
-
"maxBytes": 10 * 1024 * 1024, # 10 MB
|
|
901
|
-
"backupCount": 5,
|
|
902
|
-
"formatter": "verbose",
|
|
903
|
-
},
|
|
904
|
-
},
|
|
905
|
-
"loggers": {
|
|
906
|
-
"django": {
|
|
907
|
-
"handlers": ["file"],
|
|
908
|
-
"level": "INFO",
|
|
909
|
-
"propagate": False,
|
|
910
|
-
},
|
|
911
|
-
"myapp": {
|
|
912
|
-
"handlers": ["file"],
|
|
913
|
-
"level": "DEBUG" if DEBUG else "INFO",
|
|
914
|
-
"propagate": False,
|
|
915
|
-
},
|
|
916
|
-
},
|
|
917
|
-
}
|
|
918
|
-
```
|
|
919
|
-
|
|
920
|
-
---
|
|
921
|
-
|
|
922
|
-
## Review Checklist
|
|
923
|
-
|
|
924
|
-
### 安全审查
|
|
925
|
-
|
|
926
|
-
- [ ] 没有使用 `mark_safe` 渲染未转义的用户输入
|
|
927
|
-
- [ ] CSRF 中间件已启用,没有 `@csrf_exempt`
|
|
928
|
-
- [ ] Session 和 CSRF cookie 设置 `Secure`, `HttpOnly`, `SameSite`
|
|
929
|
-
- [ ] SQL 查询使用参数化(ORM 或参数化 `raw()`),无字符串拼接
|
|
930
|
-
- [ ] 文件上传有类型和大小限制
|
|
931
|
-
- [ ] `SECRET_KEY` 从环境变量读取,不在代码仓库中
|
|
932
|
-
- [ ] `DEBUG = False` 在生产环境
|
|
933
|
-
|
|
934
|
-
### HTTPS 与安全头
|
|
935
|
-
|
|
936
|
-
- [ ] `SECURE_SSL_REDIRECT = True`
|
|
937
|
-
- [ ] `SECURE_HSTS_SECONDS` 已设置(≥ 31536000)
|
|
938
|
-
- [ ] `SECURE_CONTENT_TYPE_NOSNIFF = True`
|
|
939
|
-
- [ ] `X_FRAME_OPTIONS` 设置为 `DENY` 或 `SAMEORIGIN`
|
|
940
|
-
- [ ] `ALLOWED_HOSTS` 不包含 `"*"`
|
|
941
|
-
- [ ] 数据库连接使用 SSL
|
|
942
|
-
|
|
943
|
-
### N+1 查询
|
|
944
|
-
|
|
945
|
-
- [ ] ForeignKey 关系使用 `select_related`
|
|
946
|
-
- [ ] M2M / 反向关系使用 `prefetch_related`
|
|
947
|
-
- [ ] 没有在循环中访问关联对象
|
|
948
|
-
- [ ] 使用 `count()` 代替 `len(queryset)` 做计数
|
|
949
|
-
- [ ] 使用 `exists()` 代替 `if queryset` 做存在性检查
|
|
950
|
-
- [ ] 大数据集使用 `only()` / `defer()` 或 `values()` 减少查询字段
|
|
951
|
-
- [ ] 切片后的 QuerySet 不重复迭代
|
|
952
|
-
|
|
953
|
-
### Serializer
|
|
954
|
-
|
|
955
|
-
- [ ] 不使用 `fields = "__all__"` 在敏感模型上
|
|
956
|
-
- [ ] 密码字段标记 `write_only=True`
|
|
957
|
-
- [ ] 有字段级和对象级验证
|
|
958
|
-
- [ ] 嵌套写入实现了 `create()` / `update()` 或使用 `read_only=True`
|
|
959
|
-
- [ ] 计算字段和自动字段在 `read_only_fields` 中
|
|
960
|
-
- [ ] Serializer 不包含不应被修改的字段
|
|
961
|
-
|
|
962
|
-
### ViewSet
|
|
963
|
-
|
|
964
|
-
- [ ] 只读场景使用 `ReadOnlyModelViewSet`
|
|
965
|
-
- [ ] `get_queryset()` 限定当前用户数据范围
|
|
966
|
-
- [ ] 设置了 `permission_classes`
|
|
967
|
-
- [ ] 创建时用 `perform_create()` 自动设置 owner/author
|
|
968
|
-
- [ ] 配置了分页(全局或 ViewSet 级别)
|
|
969
|
-
- [ ] 配置了节流(throttling)
|
|
970
|
-
|
|
971
|
-
### 异步视图
|
|
972
|
-
|
|
973
|
-
- [ ] async 视图中不直接调用同步 ORM(用 `aget`/`afilter`/`sync_to_async`)
|
|
974
|
-
- [ ] 所有异步调用都有 `await`
|
|
975
|
-
- [ ] `transaction.atomic()` 用 `sync_to_async` 包装
|
|
976
|
-
- [ ] 中间件标记 `async_capable = True` 以避免降级
|
|
977
|
-
- [ ] 大型 QuerySet 使用 `async for` + `aiterator()`
|
|
978
|
-
|
|
979
|
-
### 生产配置
|
|
980
|
-
|
|
981
|
-
- [ ] `CORS_ALLOWED_ORIGINS` 不使用 `CORS_ALLOW_ALL_ORIGINS = True`
|
|
982
|
-
- [ ] 密码验证器已配置(最小长度、常见密码检查)
|
|
983
|
-
- [ ] Session 过期时间合理(`SESSION_COOKIE_AGE`)
|
|
984
|
-
- [ ] 日志配置使用 RotatingFileHandler,不在生产环境输出到 stdout
|
|
985
|
-
- [ ] 数据库连接使用 `CONN_MAX_AGE` 持久连接
|