@humanbased/crosscheck 1.2.0 → 1.3.0-beta.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +182 -375
  3. package/README.zh.md +1 -1
  4. package/assets/icon-256.png +0 -0
  5. package/assets/linear-comment.svg +18 -0
  6. package/assets/linear-onboard.svg +30 -0
  7. package/assets/linear-status.svg +23 -0
  8. package/assets/linear-test.svg +34 -0
  9. package/assets/skills/code-review/.crosscheck-skill.json +9 -0
  10. package/assets/skills/code-review/LICENSE +21 -0
  11. package/assets/skills/code-review/SKILL.md +89 -0
  12. package/assets/skills/code-review/agents/openai.yaml +3 -0
  13. package/assets/skills/code-review-skill/.crosscheck-skill.json +9 -0
  14. package/assets/skills/code-review-skill/LICENSE +21 -0
  15. package/assets/skills/code-review-skill/SKILL.md +231 -0
  16. package/assets/skills/code-review-skill/assets/pr-review-template.md +137 -0
  17. package/assets/skills/code-review-skill/assets/review-checklist.md +123 -0
  18. package/assets/skills/code-review-skill/reference/angular.md +768 -0
  19. package/assets/skills/code-review-skill/reference/architecture-review-guide.md +472 -0
  20. package/assets/skills/code-review-skill/reference/c.md +890 -0
  21. package/assets/skills/code-review-skill/reference/code-quality-universal.md +488 -0
  22. package/assets/skills/code-review-skill/reference/code-review-best-practices.md +136 -0
  23. package/assets/skills/code-review-skill/reference/common-bugs-checklist.md +286 -0
  24. package/assets/skills/code-review-skill/reference/cpp.md +893 -0
  25. package/assets/skills/code-review-skill/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  26. package/assets/skills/code-review-skill/reference/cross-cutting/error-handling-principles.md +492 -0
  27. package/assets/skills/code-review-skill/reference/cross-cutting/n-plus-one-queries.md +309 -0
  28. package/assets/skills/code-review-skill/reference/cross-cutting/sql-injection-prevention.md +308 -0
  29. package/assets/skills/code-review-skill/reference/cross-cutting/xss-prevention.md +264 -0
  30. package/assets/skills/code-review-skill/reference/csharp.md +525 -0
  31. package/assets/skills/code-review-skill/reference/css-less-sass.md +661 -0
  32. package/assets/skills/code-review-skill/reference/django.md +985 -0
  33. package/assets/skills/code-review-skill/reference/fastapi.md +580 -0
  34. package/assets/skills/code-review-skill/reference/go.md +993 -0
  35. package/assets/skills/code-review-skill/reference/java.md +409 -0
  36. package/assets/skills/code-review-skill/reference/java8.md +586 -0
  37. package/assets/skills/code-review-skill/reference/kotlin.md +1018 -0
  38. package/assets/skills/code-review-skill/reference/nestjs.md +593 -0
  39. package/assets/skills/code-review-skill/reference/performance-review-guide.md +816 -0
  40. package/assets/skills/code-review-skill/reference/php.md +684 -0
  41. package/assets/skills/code-review-skill/reference/python.md +1073 -0
  42. package/assets/skills/code-review-skill/reference/qt.md +757 -0
  43. package/assets/skills/code-review-skill/reference/react.md +871 -0
  44. package/assets/skills/code-review-skill/reference/ruby.md +964 -0
  45. package/assets/skills/code-review-skill/reference/rust.md +846 -0
  46. package/assets/skills/code-review-skill/reference/security-review-guide.md +494 -0
  47. package/assets/skills/code-review-skill/reference/svelte.md +1064 -0
  48. package/assets/skills/code-review-skill/reference/swift.md +936 -0
  49. package/assets/skills/code-review-skill/reference/typescript.md +1016 -0
  50. package/assets/skills/code-review-skill/reference/vue.md +924 -0
  51. package/assets/skills/code-review-skill/reference/zig.md +440 -0
  52. package/assets/skills/code-review-skill/scripts/pr-analyzer.py +435 -0
  53. package/assets/skills/code-review-skill/scripts/test_pr_analyzer.py +380 -0
  54. package/assets/skills/codebase-design/.crosscheck-skill.json +9 -0
  55. package/assets/skills/codebase-design/DEEPENING.md +37 -0
  56. package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  57. package/assets/skills/codebase-design/LICENSE +21 -0
  58. package/assets/skills/codebase-design/SKILL.md +114 -0
  59. package/assets/skills/codebase-design/agents/openai.yaml +3 -0
  60. package/assets/skills/diagnosing-bugs/.crosscheck-skill.json +9 -0
  61. package/assets/skills/diagnosing-bugs/LICENSE +21 -0
  62. package/assets/skills/diagnosing-bugs/SKILL.md +134 -0
  63. package/assets/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  64. package/assets/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  65. package/crosscheck.config.example.yml +101 -9
  66. package/dist/__tests__/board.test.js +11 -0
  67. package/dist/__tests__/board.test.js.map +1 -1
  68. package/dist/__tests__/can-write-verdict.test.d.ts +2 -0
  69. package/dist/__tests__/can-write-verdict.test.d.ts.map +1 -0
  70. package/dist/__tests__/can-write-verdict.test.js +31 -0
  71. package/dist/__tests__/can-write-verdict.test.js.map +1 -0
  72. package/dist/__tests__/codex.test.js +14 -27
  73. package/dist/__tests__/codex.test.js.map +1 -1
  74. package/dist/__tests__/comment-bodies.test.js +49 -1
  75. package/dist/__tests__/comment-bodies.test.js.map +1 -1
  76. package/dist/__tests__/conflict-resolve.test.js +44 -1
  77. package/dist/__tests__/conflict-resolve.test.js.map +1 -1
  78. package/dist/__tests__/fix.test.js +33 -0
  79. package/dist/__tests__/fix.test.js.map +1 -1
  80. package/dist/__tests__/linear-branding.test.d.ts +2 -0
  81. package/dist/__tests__/linear-branding.test.d.ts.map +1 -0
  82. package/dist/__tests__/linear-branding.test.js +156 -0
  83. package/dist/__tests__/linear-branding.test.js.map +1 -0
  84. package/dist/__tests__/linear-client.test.d.ts +2 -0
  85. package/dist/__tests__/linear-client.test.d.ts.map +1 -0
  86. package/dist/__tests__/linear-client.test.js +120 -0
  87. package/dist/__tests__/linear-client.test.js.map +1 -0
  88. package/dist/__tests__/linear-comment.test.d.ts +2 -0
  89. package/dist/__tests__/linear-comment.test.d.ts.map +1 -0
  90. package/dist/__tests__/linear-comment.test.js +151 -0
  91. package/dist/__tests__/linear-comment.test.js.map +1 -0
  92. package/dist/__tests__/linear-identity.test.d.ts +2 -0
  93. package/dist/__tests__/linear-identity.test.d.ts.map +1 -0
  94. package/dist/__tests__/linear-identity.test.js +253 -0
  95. package/dist/__tests__/linear-identity.test.js.map +1 -0
  96. package/dist/__tests__/linear-notify.test.d.ts +2 -0
  97. package/dist/__tests__/linear-notify.test.d.ts.map +1 -0
  98. package/dist/__tests__/linear-notify.test.js +144 -0
  99. package/dist/__tests__/linear-notify.test.js.map +1 -0
  100. package/dist/__tests__/linear-ref.test.d.ts +2 -0
  101. package/dist/__tests__/linear-ref.test.d.ts.map +1 -0
  102. package/dist/__tests__/linear-ref.test.js +261 -0
  103. package/dist/__tests__/linear-ref.test.js.map +1 -0
  104. package/dist/__tests__/linear-test-ref.test.d.ts +2 -0
  105. package/dist/__tests__/linear-test-ref.test.d.ts.map +1 -0
  106. package/dist/__tests__/linear-test-ref.test.js +81 -0
  107. package/dist/__tests__/linear-test-ref.test.js.map +1 -0
  108. package/dist/__tests__/linear-verify.test.d.ts +2 -0
  109. package/dist/__tests__/linear-verify.test.d.ts.map +1 -0
  110. package/dist/__tests__/linear-verify.test.js +132 -0
  111. package/dist/__tests__/linear-verify.test.js.map +1 -0
  112. package/dist/__tests__/linear-worker.test.d.ts +2 -0
  113. package/dist/__tests__/linear-worker.test.d.ts.map +1 -0
  114. package/dist/__tests__/linear-worker.test.js +83 -0
  115. package/dist/__tests__/linear-worker.test.js.map +1 -0
  116. package/dist/__tests__/linear-write-possible.test.d.ts +2 -0
  117. package/dist/__tests__/linear-write-possible.test.d.ts.map +1 -0
  118. package/dist/__tests__/linear-write-possible.test.js +30 -0
  119. package/dist/__tests__/linear-write-possible.test.js.map +1 -0
  120. package/dist/__tests__/onboard-preservation.test.js +59 -3
  121. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  122. package/dist/__tests__/optimize.test.js +2 -0
  123. package/dist/__tests__/optimize.test.js.map +1 -1
  124. package/dist/__tests__/pr-status.test.js +163 -2
  125. package/dist/__tests__/pr-status.test.js.map +1 -1
  126. package/dist/__tests__/pr-workflow-state.test.js +102 -1
  127. package/dist/__tests__/pr-workflow-state.test.js.map +1 -1
  128. package/dist/__tests__/repo-picker.test.js +7 -1
  129. package/dist/__tests__/repo-picker.test.js.map +1 -1
  130. package/dist/__tests__/repository-guidance.test.d.ts +2 -0
  131. package/dist/__tests__/repository-guidance.test.d.ts.map +1 -0
  132. package/dist/__tests__/repository-guidance.test.js +107 -0
  133. package/dist/__tests__/repository-guidance.test.js.map +1 -0
  134. package/dist/__tests__/review-comment-body.test.js +35 -0
  135. package/dist/__tests__/review-comment-body.test.js.map +1 -1
  136. package/dist/__tests__/review-models.test.js +19 -3
  137. package/dist/__tests__/review-models.test.js.map +1 -1
  138. package/dist/__tests__/review-strategy.test.d.ts +2 -0
  139. package/dist/__tests__/review-strategy.test.d.ts.map +1 -0
  140. package/dist/__tests__/review-strategy.test.js +397 -0
  141. package/dist/__tests__/review-strategy.test.js.map +1 -0
  142. package/dist/__tests__/runner.test.js +29 -1
  143. package/dist/__tests__/runner.test.js.map +1 -1
  144. package/dist/__tests__/skill-attribution.test.d.ts +2 -0
  145. package/dist/__tests__/skill-attribution.test.d.ts.map +1 -0
  146. package/dist/__tests__/skill-attribution.test.js +53 -0
  147. package/dist/__tests__/skill-attribution.test.js.map +1 -0
  148. package/dist/__tests__/skill-broker.test.d.ts +2 -0
  149. package/dist/__tests__/skill-broker.test.d.ts.map +1 -0
  150. package/dist/__tests__/skill-broker.test.js +144 -0
  151. package/dist/__tests__/skill-broker.test.js.map +1 -0
  152. package/dist/__tests__/skill-catalog.test.d.ts +2 -0
  153. package/dist/__tests__/skill-catalog.test.d.ts.map +1 -0
  154. package/dist/__tests__/skill-catalog.test.js +40 -0
  155. package/dist/__tests__/skill-catalog.test.js.map +1 -0
  156. package/dist/__tests__/skill-installer.test.d.ts +2 -0
  157. package/dist/__tests__/skill-installer.test.d.ts.map +1 -0
  158. package/dist/__tests__/skill-installer.test.js +96 -0
  159. package/dist/__tests__/skill-installer.test.js.map +1 -0
  160. package/dist/__tests__/skills-config.test.d.ts +2 -0
  161. package/dist/__tests__/skills-config.test.d.ts.map +1 -0
  162. package/dist/__tests__/skills-config.test.js +12 -0
  163. package/dist/__tests__/skills-config.test.js.map +1 -0
  164. package/dist/cli.js +29 -0
  165. package/dist/cli.js.map +1 -1
  166. package/dist/commands/detect-step.d.ts.map +1 -1
  167. package/dist/commands/detect-step.js +4 -0
  168. package/dist/commands/detect-step.js.map +1 -1
  169. package/dist/commands/kickass.d.ts.map +1 -1
  170. package/dist/commands/kickass.js +3 -2
  171. package/dist/commands/kickass.js.map +1 -1
  172. package/dist/commands/linear-test.d.ts +18 -0
  173. package/dist/commands/linear-test.d.ts.map +1 -0
  174. package/dist/commands/linear-test.js +130 -0
  175. package/dist/commands/linear-test.js.map +1 -0
  176. package/dist/commands/onboard.d.ts +36 -3
  177. package/dist/commands/onboard.d.ts.map +1 -1
  178. package/dist/commands/onboard.js +233 -42
  179. package/dist/commands/onboard.js.map +1 -1
  180. package/dist/commands/review.d.ts.map +1 -1
  181. package/dist/commands/review.js +65 -6
  182. package/dist/commands/review.js.map +1 -1
  183. package/dist/commands/run.d.ts.map +1 -1
  184. package/dist/commands/run.js +51 -7
  185. package/dist/commands/run.js.map +1 -1
  186. package/dist/commands/skill.d.ts +2 -0
  187. package/dist/commands/skill.d.ts.map +1 -0
  188. package/dist/commands/skill.js +16 -0
  189. package/dist/commands/skill.js.map +1 -0
  190. package/dist/commands/status.d.ts.map +1 -1
  191. package/dist/commands/status.js +53 -1
  192. package/dist/commands/status.js.map +1 -1
  193. package/dist/commands/watch.d.ts.map +1 -1
  194. package/dist/commands/watch.js +169 -64
  195. package/dist/commands/watch.js.map +1 -1
  196. package/dist/config/loader.d.ts +3 -1
  197. package/dist/config/loader.d.ts.map +1 -1
  198. package/dist/config/loader.js +13 -0
  199. package/dist/config/loader.js.map +1 -1
  200. package/dist/config/review-model-tiers.json +3 -3
  201. package/dist/config/review-strategy.json +204 -0
  202. package/dist/config/schema.d.ts +261 -15
  203. package/dist/config/schema.d.ts.map +1 -1
  204. package/dist/config/schema.js +90 -8
  205. package/dist/config/schema.js.map +1 -1
  206. package/dist/github/client.d.ts +21 -1
  207. package/dist/github/client.d.ts.map +1 -1
  208. package/dist/github/client.js +46 -7
  209. package/dist/github/client.js.map +1 -1
  210. package/dist/github/webhook.d.ts +4 -0
  211. package/dist/github/webhook.d.ts.map +1 -1
  212. package/dist/github/webhook.js.map +1 -1
  213. package/dist/issues/ticket-ref.d.ts.map +1 -1
  214. package/dist/issues/ticket-ref.js +6 -5
  215. package/dist/issues/ticket-ref.js.map +1 -1
  216. package/dist/lib/annotation.d.ts +7 -0
  217. package/dist/lib/annotation.d.ts.map +1 -1
  218. package/dist/lib/annotation.js +11 -1
  219. package/dist/lib/annotation.js.map +1 -1
  220. package/dist/lib/board.d.ts +3 -0
  221. package/dist/lib/board.d.ts.map +1 -1
  222. package/dist/lib/board.js +4 -2
  223. package/dist/lib/board.js.map +1 -1
  224. package/dist/lib/clone.d.ts +1 -0
  225. package/dist/lib/clone.d.ts.map +1 -1
  226. package/dist/lib/clone.js +32 -10
  227. package/dist/lib/clone.js.map +1 -1
  228. package/dist/lib/comment-bodies.d.ts +37 -0
  229. package/dist/lib/comment-bodies.d.ts.map +1 -1
  230. package/dist/lib/comment-bodies.js +47 -9
  231. package/dist/lib/comment-bodies.js.map +1 -1
  232. package/dist/lib/pr-status.d.ts.map +1 -1
  233. package/dist/lib/pr-status.js +36 -2
  234. package/dist/lib/pr-status.js.map +1 -1
  235. package/dist/lib/pr-workflow-state.d.ts +5 -0
  236. package/dist/lib/pr-workflow-state.d.ts.map +1 -1
  237. package/dist/lib/pr-workflow-state.js +36 -1
  238. package/dist/lib/pr-workflow-state.js.map +1 -1
  239. package/dist/lib/repo-picker.d.ts +3 -0
  240. package/dist/lib/repo-picker.d.ts.map +1 -1
  241. package/dist/lib/repo-picker.js +45 -9
  242. package/dist/lib/repo-picker.js.map +1 -1
  243. package/dist/lib/repository-guidance.d.ts +2 -0
  244. package/dist/lib/repository-guidance.d.ts.map +1 -0
  245. package/dist/lib/repository-guidance.js +55 -0
  246. package/dist/lib/repository-guidance.js.map +1 -0
  247. package/dist/lib/review-models.d.ts +15 -2
  248. package/dist/lib/review-models.d.ts.map +1 -1
  249. package/dist/lib/review-models.js +26 -6
  250. package/dist/lib/review-models.js.map +1 -1
  251. package/dist/lib/review-strategy.d.ts +92 -0
  252. package/dist/lib/review-strategy.d.ts.map +1 -0
  253. package/dist/lib/review-strategy.js +282 -0
  254. package/dist/lib/review-strategy.js.map +1 -0
  255. package/dist/lib/runner.d.ts +92 -0
  256. package/dist/lib/runner.d.ts.map +1 -1
  257. package/dist/lib/runner.js +470 -54
  258. package/dist/lib/runner.js.map +1 -1
  259. package/dist/lib/workflow.d.ts +9 -0
  260. package/dist/lib/workflow.d.ts.map +1 -1
  261. package/dist/lib/workflow.js +20 -0
  262. package/dist/lib/workflow.js.map +1 -1
  263. package/dist/linear/client.d.ts +18 -0
  264. package/dist/linear/client.d.ts.map +1 -0
  265. package/dist/linear/client.js +67 -0
  266. package/dist/linear/client.js.map +1 -0
  267. package/dist/linear/comment.d.ts +20 -0
  268. package/dist/linear/comment.d.ts.map +1 -0
  269. package/dist/linear/comment.js +57 -0
  270. package/dist/linear/comment.js.map +1 -0
  271. package/dist/linear/identity.d.ts +59 -0
  272. package/dist/linear/identity.d.ts.map +1 -0
  273. package/dist/linear/identity.js +187 -0
  274. package/dist/linear/identity.js.map +1 -0
  275. package/dist/linear/notify.d.ts +35 -0
  276. package/dist/linear/notify.d.ts.map +1 -0
  277. package/dist/linear/notify.js +76 -0
  278. package/dist/linear/notify.js.map +1 -0
  279. package/dist/linear/ref.d.ts +13 -0
  280. package/dist/linear/ref.d.ts.map +1 -0
  281. package/dist/linear/ref.js +90 -0
  282. package/dist/linear/ref.js.map +1 -0
  283. package/dist/linear/verify.d.ts +26 -0
  284. package/dist/linear/verify.d.ts.map +1 -0
  285. package/dist/linear/verify.js +67 -0
  286. package/dist/linear/verify.js.map +1 -0
  287. package/dist/reviewers/claude.d.ts +4 -1
  288. package/dist/reviewers/claude.d.ts.map +1 -1
  289. package/dist/reviewers/claude.js +39 -7
  290. package/dist/reviewers/claude.js.map +1 -1
  291. package/dist/reviewers/codex.d.ts +3 -1
  292. package/dist/reviewers/codex.d.ts.map +1 -1
  293. package/dist/reviewers/codex.js +76 -70
  294. package/dist/reviewers/codex.js.map +1 -1
  295. package/dist/reviewers/conflict-resolve.d.ts +3 -1
  296. package/dist/reviewers/conflict-resolve.d.ts.map +1 -1
  297. package/dist/reviewers/conflict-resolve.js +21 -6
  298. package/dist/reviewers/conflict-resolve.js.map +1 -1
  299. package/dist/reviewers/fix.d.ts +5 -2
  300. package/dist/reviewers/fix.d.ts.map +1 -1
  301. package/dist/reviewers/fix.js +26 -10
  302. package/dist/reviewers/fix.js.map +1 -1
  303. package/dist/skills/attribution.d.ts +4 -0
  304. package/dist/skills/attribution.d.ts.map +1 -0
  305. package/dist/skills/attribution.js +14 -0
  306. package/dist/skills/attribution.js.map +1 -0
  307. package/dist/skills/broker-server.d.ts +2 -0
  308. package/dist/skills/broker-server.d.ts.map +1 -0
  309. package/dist/skills/broker-server.js +17 -0
  310. package/dist/skills/broker-server.js.map +1 -0
  311. package/dist/skills/broker.d.ts +42 -0
  312. package/dist/skills/broker.d.ts.map +1 -0
  313. package/dist/skills/broker.js +285 -0
  314. package/dist/skills/broker.js.map +1 -0
  315. package/dist/skills/catalog.d.ts +28 -0
  316. package/dist/skills/catalog.d.ts.map +1 -0
  317. package/dist/skills/catalog.js +104 -0
  318. package/dist/skills/catalog.js.map +1 -0
  319. package/dist/skills/installer.d.ts +10 -0
  320. package/dist/skills/installer.d.ts.map +1 -0
  321. package/dist/skills/installer.js +138 -0
  322. package/dist/skills/installer.js.map +1 -0
  323. package/dist/skills/integrity.d.ts +4 -0
  324. package/dist/skills/integrity.d.ts.map +1 -0
  325. package/dist/skills/integrity.js +36 -0
  326. package/dist/skills/integrity.js.map +1 -0
  327. package/docs/dynamic-thoroughness.md +738 -0
  328. package/docs/linear-identity-contract.md +139 -0
  329. package/docs/linear-identity.md +293 -0
  330. package/get-started.md +223 -11
  331. package/package.json +4 -3
@@ -0,0 +1,985 @@
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` 持久连接