@umacloud/knowledge 1.0.1
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/00-governance/governance-capabilities.md +557 -0
- package/00-governance/knowledge-map.md +39 -0
- package/00-governance/maintenance-policy.md +76 -0
- package/00-governance/review-checklist.md +81 -0
- package/README.md +13 -0
- package/ai/01-standards/agent-development-complete.md +691 -0
- package/ai/01-standards/llm-application-complete.md +488 -0
- package/ai/01-standards/mlops-complete.md +798 -0
- package/ai/01-standards/prompt-engineering-complete.md +646 -0
- package/ai/01-standards/rag-architecture-complete.md +649 -0
- package/ai/02-playbooks/llm-evaluation-playbook.md +847 -0
- package/ai/03-checklists/ai-project-checklist.md +215 -0
- package/ai/04-antipatterns/ai-antipatterns.md +661 -0
- package/ai/05-cases/case-rag-production.md +147 -0
- package/ai/06-glossary/ai-glossary.md +162 -0
- package/ai/agent-evaluation-benchmark.md +53 -0
- package/ai/ai-agent-memory-context-management.md +41 -0
- package/ai/ai-cost-capacity-optimization-playbook.md +42 -0
- package/ai/ai-data-security-and-compliance-playbook.md +37 -0
- package/ai/ai-domain-index-and-checklist.md +40 -0
- package/ai/ai-governance-maturity-model.md +50 -0
- package/ai/ai-model-selection-and-routing-strategy.md +47 -0
- package/ai/ai-observability-and-oncall-runbook.md +52 -0
- package/ai/ai-rag-engineering-playbook.md +42 -0
- package/ai/ai-red-team-and-safety-evaluation.md +42 -0
- package/ai/ai-release-readiness-and-rollback-gate.md +42 -0
- package/ai/llm-agent-engineering-deep-dive.md +57 -0
- package/ai/prompt-and-tool-guardrails.md +52 -0
- package/api/01-standards/enterprise-api-standards.md +198 -0
- package/api/01-standards/rest-api-design-guide.md +63 -0
- package/api/02-playbooks/api-pagination-playbook.md +93 -0
- package/api/02-playbooks/graphql-production-playbook.md +176 -0
- package/api/03-checklists/api-review-checklist.md +55 -0
- package/api/04-antipatterns/api-antipatterns.md +112 -0
- package/architecture/01-standards/api-gateway-patterns.md +496 -0
- package/architecture/01-standards/cloud-native-patterns.md +644 -0
- package/architecture/01-standards/distributed-systems-patterns.md +591 -0
- package/architecture/01-standards/event-driven-architecture.md +595 -0
- package/architecture/01-standards/microservices-patterns-complete.md +968 -0
- package/architecture/01-standards/microservices-patterns.md +495 -0
- package/architecture/01-standards/system-design-interview.md +664 -0
- package/architecture/02-playbooks/microservices-patterns-playbook.md +137 -0
- package/architecture/02-playbooks/migration-playbook.md +780 -0
- package/architecture/02-playbooks/system-design-playbook.md +779 -0
- package/architecture/03-checklists/architecture-decision-checklist.md +297 -0
- package/architecture/04-antipatterns/architecture-antipatterns.md +417 -0
- package/architecture/05-cases/case-netflix-microservices.md +413 -0
- package/architecture/06-glossary/architecture-glossary.md +164 -0
- package/architecture/adr-template-and-examples.md +38 -0
- package/architecture/api-gateway-deep-dive.md +1291 -0
- package/architecture/configuration-management.md +1162 -0
- package/architecture/distributed-transactions.md +1220 -0
- package/architecture/microservices-complete.md +735 -0
- package/architecture/resilience-and-disaster-patterns.md +37 -0
- package/architecture/service-governance.md +1198 -0
- package/architecture/system-architecture-deep-dive.md +37 -0
- package/backend/01-standards/analytics-and-growth.md +65 -0
- package/backend/01-standards/api-and-error-conventions.md +120 -0
- package/backend/01-standards/application-layering-and-packaging.md +160 -0
- package/backend/01-standards/auth-implementation.md +104 -0
- package/backend/01-standards/backend-framework-idioms.md +74 -0
- package/backend/01-standards/background-jobs-and-async.md +66 -0
- package/backend/01-standards/caching-strategies-complete.md +390 -0
- package/backend/01-standards/config-and-observability.md +77 -0
- package/backend/01-standards/data-modeling-and-persistence.md +94 -0
- package/backend/01-standards/django-complete.md +1765 -0
- package/backend/01-standards/email-and-notifications.md +64 -0
- package/backend/01-standards/fastapi-complete.md +925 -0
- package/backend/01-standards/file-upload-and-storage.md +66 -0
- package/backend/01-standards/graphql-api-complete.md +416 -0
- package/backend/01-standards/llm-application-standard.md +78 -0
- package/backend/01-standards/message-queue-patterns.md +379 -0
- package/backend/01-standards/microservices-and-distributed.md +78 -0
- package/backend/01-standards/nestjs-complete.md +2167 -0
- package/backend/01-standards/payment-integration.md +80 -0
- package/backend/01-standards/rate-limiting-complete.md +451 -0
- package/backend/01-standards/realtime-and-websocket.md +65 -0
- package/backend/01-standards/search-and-filtering.md +64 -0
- package/backend/01-standards/spring-boot-complete.md +445 -0
- package/backend/02-playbooks/api-design-playbook.md +718 -0
- package/backend/02-playbooks/email-send-playbook.md +130 -0
- package/backend/02-playbooks/file-upload-s3-playbook.md +153 -0
- package/backend/02-playbooks/typescript-enterprise-playbook.md +133 -0
- package/backend/02-playbooks/websocket-realtime-playbook.md +154 -0
- package/backend/03-checklists/api-launch-checklist.md +189 -0
- package/backend/04-antipatterns/backend-antipatterns.md +1051 -0
- package/blockchain/01-standards/blockchain-basics.md +557 -0
- package/blockchain/01-standards/smart-contract-development.md +1315 -0
- package/cicd/01-standards/deployment-and-delivery-standard.md +96 -0
- package/cicd/01-standards/github-actions-complete.md +473 -0
- package/cicd/01-standards/release-and-store-submission.md +75 -0
- package/cicd/02-playbooks/cicd-pipeline-playbook.md +144 -0
- package/cicd/02-playbooks/release-management-playbook.md +605 -0
- package/cicd/03-checklists/pipeline-security-checklist.md +168 -0
- package/cicd/04-antipatterns/cicd-antipatterns.md +589 -0
- package/cicd/05-cases/case-deployment-automation.md +221 -0
- package/cicd/05-cases/case-gitops-transformation.md +212 -0
- package/cicd/06-glossary/cicd-glossary.md +114 -0
- package/cicd/cicd-blueprint-deep-dive.md +38 -0
- package/cicd/release-readiness-gate.md +37 -0
- package/cloud-native/01-standards/container-security.md +741 -0
- package/cloud-native/01-standards/kubernetes-complete.md +812 -0
- package/cloud-native/02-playbooks/api-gateway-playbook.md +155 -0
- package/cloud-native/02-playbooks/gitops-with-argocd.md +760 -0
- package/cloud-native/02-playbooks/k8s-troubleshooting-playbook.md +1942 -0
- package/cloud-native/02-playbooks/message-queue-playbook.md +129 -0
- package/cloud-native/02-playbooks/multicloud-governance.md +726 -0
- package/cloud-native/02-playbooks/serverless-patterns.md +788 -0
- package/cloud-native/02-playbooks/service-mesh-playbook.md +612 -0
- package/cloud-native/02-playbooks/terraform-iac-playbook.md +143 -0
- package/cloud-native/03-checklists/container-security-checklist.md +431 -0
- package/cloud-native/03-checklists/k8s-production-readiness-checklist.md +460 -0
- package/cloud-native/04-antipatterns/container-antipatterns.md +660 -0
- package/cloud-native/04-antipatterns/k8s-antipatterns.md +743 -0
- package/cloud-native/05-cases/case-k8s-migration.md +478 -0
- package/cloud-native/05-cases/case-k8s-scaling.md +642 -0
- package/cloud-native/05-cases/case-k8s-security-incident.md +397 -0
- package/cloud-native/06-glossary/cloud-native-glossary.md +337 -0
- package/cross-platform/01-standards/cross-platform-frameworks.md +83 -0
- package/cross-platform/01-standards/platform-selection-and-architecture.md +77 -0
- package/data/01-standards/elasticsearch-complete.md +2098 -0
- package/data/01-standards/postgresql-complete.md +1613 -0
- package/data/01-standards/redis-complete.md +1527 -0
- package/data/02-playbooks/database-optimization-playbook.md +403 -0
- package/data/02-playbooks/elasticsearch-production-playbook.md +132 -0
- package/data/03-checklists/database-launch-checklist.md +187 -0
- package/data/04-antipatterns/database-antipatterns.md +873 -0
- package/data/05-cases/case-database-migration.md +310 -0
- package/data/06-glossary/database-glossary.md +440 -0
- package/data/data-governance-and-modeling-deep-dive.md +39 -0
- package/data-engineering/01-standards/airflow-complete.md +523 -0
- package/data-engineering/01-standards/kafka-complete.md +1521 -0
- package/data-engineering/02-playbooks/spark-etl-playbook.md +496 -0
- package/data-engineering/03-checklists/pipeline-launch-checklist.md +194 -0
- package/data-engineering/04-antipatterns/data-pipeline-antipatterns.md +684 -0
- package/data-engineering/05-cases/case-real-time-pipeline.md +355 -0
- package/data-engineering/06-glossary/data-engineering-glossary.md +429 -0
- package/database/01-standards/database-schema-standards.md +147 -0
- package/database/02-playbooks/postgresql-optimization-quick.md +52 -0
- package/database/02-playbooks/postgresql-performance-optimization.md +58 -0
- package/database/02-playbooks/postgresql-production-playbook.md +146 -0
- package/database/02-playbooks/redis-caching-playbook.md +117 -0
- package/database/03-checklists/database-review-checklist.md +50 -0
- package/database/04-antipatterns/database-antipatterns.md +112 -0
- package/design/01-standards/ui-design-system-complete.md +423 -0
- package/design/02-playbooks/design-handoff-playbook.md +254 -0
- package/design/02-playbooks/design-review-playbook.md +388 -0
- package/design/03-checklists/design-review-checklist.md +246 -0
- package/design/04-antipatterns/design-antipatterns.md +378 -0
- package/design/05-cases/case-design-system-adoption.md +328 -0
- package/design/06-glossary/design-glossary.md +329 -0
- package/design/ui-full-lifecycle-cross-platform-playbook.md +571 -0
- package/design/ux-system-deep-dive.md +38 -0
- package/design-systems/00-craft-rules.md +71 -0
- package/design-systems/aesthetic-families.md +43 -0
- package/design-systems/anti-ai-slop.md +162 -0
- package/design-systems/bold-geometric.md +120 -0
- package/design-systems/brutalist-bold.md +103 -0
- package/design-systems/editorial-clean.md +109 -0
- package/design-systems/glass-aurora.md +108 -0
- package/design-systems/modern-minimal.md +145 -0
- package/design-systems/premium-luxury.md +106 -0
- package/design-systems/product-type-design-map.md +48 -0
- package/design-systems/soft-warm.md +123 -0
- package/design-systems/tech-utility.md +113 -0
- package/desktop/01-standards/desktop-app-standard.md +72 -0
- package/desktop/01-standards/desktop-design.md +71 -0
- package/development/00-governance/document-template.md +41 -0
- package/development/01-standards/api-versioning-strategies.md +432 -0
- package/development/01-standards/authentication-patterns-complete.md +479 -0
- package/development/01-standards/css-architecture-complete.md +550 -0
- package/development/01-standards/database-migration-strategies.md +484 -0
- package/development/01-standards/elasticsearch-complete.md +347 -0
- package/development/01-standards/git-complete.md +371 -0
- package/development/01-standards/golang-complete.md +1565 -0
- package/development/01-standards/graphql-complete.md +298 -0
- package/development/01-standards/javascript-bundlers-complete.md +469 -0
- package/development/01-standards/javascript-typescript-complete.md +528 -0
- package/development/01-standards/jest-complete.md +275 -0
- package/development/01-standards/linux-complete.md +234 -0
- package/development/01-standards/logging-observability-complete.md +526 -0
- package/development/01-standards/microservices-communication.md +502 -0
- package/development/01-standards/mongodb-complete.md +406 -0
- package/development/01-standards/oauth2-complete.md +285 -0
- package/development/01-standards/performance-optimization-complete.md +289 -0
- package/development/01-standards/playwright-complete.md +247 -0
- package/development/01-standards/postgresql-complete.md +456 -0
- package/development/01-standards/pytest-complete.md +340 -0
- package/development/01-standards/python-async-programming.md +902 -0
- package/development/01-standards/python-complete.md +956 -0
- package/development/01-standards/python-decorators-complete.md +799 -0
- package/development/01-standards/python-design-patterns.md +2854 -0
- package/development/01-standards/python-packaging-distribution.md +420 -0
- package/development/01-standards/python-testing-strategies.md +607 -0
- package/development/01-standards/python-web-frameworks-comparison.md +471 -0
- package/development/01-standards/redis-complete.md +317 -0
- package/development/01-standards/rest-api-complete.md +316 -0
- package/development/01-standards/rust-complete.md +578 -0
- package/development/01-standards/typescript-advanced-types.md +1513 -0
- package/development/01-standards/web-security-complete.md +292 -0
- package/development/02-playbooks/api-design-playbook.md +810 -0
- package/development/02-playbooks/database-migration-playbook.md +580 -0
- package/development/02-playbooks/debugging-playbook.md +692 -0
- package/development/02-playbooks/feature-delivery-playbook.md +430 -0
- package/development/02-playbooks/incident-hotfix-playbook.md +387 -0
- package/development/02-playbooks/performance-optimization-playbook.md +531 -0
- package/development/02-playbooks/performance-tuning-playbook.md +652 -0
- package/development/02-playbooks/refactor-playbook.md +403 -0
- package/development/02-playbooks/release-playbook.md +469 -0
- package/development/03-checklists/architecture-review-checklist.md +168 -0
- package/development/03-checklists/data-migration-checklist.md +157 -0
- package/development/03-checklists/oncall-handover-checklist.md +173 -0
- package/development/03-checklists/pr-checklist.md +158 -0
- package/development/03-checklists/production-readiness-checklist.md +190 -0
- package/development/03-checklists/release-readiness-checklist.md +154 -0
- package/development/03-checklists/security-review-checklist.md +182 -0
- package/development/04-antipatterns/api-antipatterns.md +657 -0
- package/development/04-antipatterns/architecture-antipatterns.md +686 -0
- package/development/04-antipatterns/backend-antipatterns.md +648 -0
- package/development/04-antipatterns/cicd-antipatterns.md +540 -0
- package/development/04-antipatterns/code-smell-antipatterns.md +571 -0
- package/development/04-antipatterns/data-antipatterns.md +658 -0
- package/development/04-antipatterns/database-antipatterns.md +578 -0
- package/development/04-antipatterns/frontend-antipatterns.md +635 -0
- package/development/04-antipatterns/reliability-antipatterns.md +700 -0
- package/development/04-antipatterns/security-antipatterns.md +747 -0
- package/development/05-cases/case-api-version-migration.md +428 -0
- package/development/05-cases/case-authorization-hardening.md +383 -0
- package/development/05-cases/case-bluegreen-rollback.md +466 -0
- package/development/05-cases/case-cache-snowball-protection.md +485 -0
- package/development/05-cases/case-ci-cd-pipeline.md +544 -0
- package/development/05-cases/case-database-scaling.md +500 -0
- package/development/05-cases/case-db-hotspot-optimization.md +487 -0
- package/development/05-cases/case-incident-mttr-reduction.md +563 -0
- package/development/05-cases/case-microservice-migration.md +375 -0
- package/development/05-cases/case-performance-optimization.md +406 -0
- package/development/05-cases/case-security-incident-response.md +345 -0
- package/development/06-glossary/full-stack-glossary.md +166 -0
- package/development/09-maturity/quarterly-audit-template.md +35 -0
- package/development/11-ui-excellence/ui-aesthetic-system.md +41 -0
- package/development/11-ui-excellence/ui-engineering-excellence.md +435 -0
- package/development/12-scenarios/development-scenarios-guide.md +565 -0
- package/development/13-implementation-assets/implementation-toolkit.md +282 -0
- package/development/13-implementation-assets/knowledge-gates-execution.md +43 -0
- package/development/14-full-lifecycle/software-lifecycle-gates.md +511 -0
- package/development/15-lifecycle-templates/project-templates-collection.md +791 -0
- package/development/api-contract-and-versioning-guide.md +36 -0
- package/development/api-governance-complete.md +43 -0
- package/development/backend-engineering-complete.md +43 -0
- package/development/code-review-quality-complete.md +43 -0
- package/development/concurrency-reliability-complete.md +43 -0
- package/development/database-engineering-complete.md +43 -0
- package/development/engineering-effectiveness-complete.md +43 -0
- package/development/engineering-standards-deep-dive.md +38 -0
- package/development/frontend-engineering-complete.md +43 -0
- package/development/performance-capacity-complete.md +43 -0
- package/development/refactor-migration-complete.md +42 -0
- package/development/refactoring-and-techdebt-playbook.md +37 -0
- package/development/security-in-development-complete.md +43 -0
- package/devops/01-standards/cicd-pipeline-complete.md +262 -0
- package/devops/01-standards/docker-complete.md +1490 -0
- package/devops/01-standards/github-actions-complete.md +337 -0
- package/devops/01-standards/kubernetes-complete.md +638 -0
- package/devops/01-standards/terraform-complete.md +2117 -0
- package/devops/02-playbooks/docker-compose-playbook.md +233 -0
- package/devops/02-playbooks/docker-k8s-production-playbook.md +186 -0
- package/devops/02-playbooks/docker-production-playbook.md +952 -0
- package/edge-iot/01-standards/edge-iot-complete.md +473 -0
- package/experts/architect/api-design.md +178 -0
- package/experts/architect/methodology.md +124 -0
- package/experts/architect/security.md +75 -0
- package/experts/backend-lead/methodology.md +216 -0
- package/experts/devops/methodology.md +160 -0
- package/experts/frontend-lead/methodology.md +178 -0
- package/experts/product-manager/industry/ecommerce.md +43 -0
- package/experts/product-manager/industry/saas.md +40 -0
- package/experts/product-manager/methodology.md +97 -0
- package/experts/qa-lead/methodology.md +123 -0
- package/experts/qa-lead/test-strategy.md +128 -0
- package/experts/uiux-designer/methodology.md +125 -0
- package/frontend/01-standards/accessibility-complete.md +532 -0
- package/frontend/01-standards/accessibility-standard.md +74 -0
- package/frontend/01-standards/admin-dashboard-and-crud.md +72 -0
- package/frontend/01-standards/design-tokens-complete.md +444 -0
- package/frontend/01-standards/forms-and-validation.md +77 -0
- package/frontend/01-standards/frontend-architecture-and-layering.md +119 -0
- package/frontend/01-standards/i18n-and-localization.md +65 -0
- package/frontend/01-standards/nextjs-complete.md +451 -0
- package/frontend/01-standards/react-complete.md +713 -0
- package/frontend/01-standards/react-hooks-complete-guide.md +1100 -0
- package/frontend/01-standards/react-hooks-complete.md +1171 -0
- package/frontend/01-standards/seo-and-web-vitals.md +77 -0
- package/frontend/01-standards/state-management-complete.md +444 -0
- package/frontend/01-standards/vue-complete.md +499 -0
- package/frontend/01-standards/vue3-complete.md +2002 -0
- package/frontend/01-standards/web-framework-best-practices.md +64 -0
- package/frontend/01-standards/web-performance-complete.md +495 -0
- package/frontend/02-playbooks/accessibility-a11y-playbook.md +161 -0
- package/frontend/02-playbooks/frontend-performance-playbook.md +707 -0
- package/frontend/02-playbooks/i18n-internationalization-playbook.md +120 -0
- package/frontend/02-playbooks/performance-optimization-playbook.md +163 -0
- package/frontend/02-playbooks/react-nextjs-production-playbook.md +167 -0
- package/frontend/02-playbooks/react-state-management-playbook.md +173 -0
- package/frontend/03-checklists/component-quality-checklist.md +166 -0
- package/frontend/03-checklists/frontend-launch-checklist.md +299 -0
- package/frontend/04-antipatterns/frontend-antipatterns.md +886 -0
- package/frontend/05-cases/case-performance-optimization.md +274 -0
- package/harmony/01-standards/harmonyos-arkts-standard.md +75 -0
- package/harmony/01-standards/harmonyos-design.md +65 -0
- package/high-quality-engineering-playbook.md +54 -0
- package/incident/01-standards/incident-response-complete.md +303 -0
- package/incident/02-playbooks/chaos-engineering-playbook.md +883 -0
- package/incident/02-playbooks/postmortem-playbook.md +398 -0
- package/incident/03-checklists/incident-readiness-checklist.md +181 -0
- package/incident/04-antipatterns/incident-antipatterns.md +490 -0
- package/incident/05-cases/case-cascade-failure.md +176 -0
- package/incident/06-glossary/incident-glossary.md +114 -0
- package/incident/postmortem-and-response-deep-dive.md +39 -0
- package/industries/ecommerce/ecommerce-complete.md +631 -0
- package/industries/education/education-complete.md +555 -0
- package/industries/fintech/fintech-complete.md +501 -0
- package/industries/gaming/gaming-complete.md +587 -0
- package/industries/healthcare/healthcare-complete.md +452 -0
- package/low-code/01-standards/low-code-complete.md +944 -0
- package/miniprogram/01-standards/ai-common-mistakes.md +61 -0
- package/miniprogram/01-standards/miniprogram-custom-navbar-capsule.md +77 -0
- package/miniprogram/01-standards/miniprogram-design.md +61 -0
- package/miniprogram/01-standards/miniprogram-standard.md +81 -0
- package/mobile/01-standards/android-material-design.md +70 -0
- package/mobile/01-standards/flutter-complete.md +384 -0
- package/mobile/01-standards/ios-design-hig.md +78 -0
- package/mobile/01-standards/mobile-app-standard.md +85 -0
- package/mobile/01-standards/react-native-complete.md +352 -0
- package/mobile/02-playbooks/mobile-cross-platform-playbook.md +175 -0
- package/mobile/02-playbooks/mobile-performance.md +473 -0
- package/mobile/03-checklists/mobile-release-checklist.md +234 -0
- package/mobile/04-antipatterns/mobile-antipatterns.md +798 -0
- package/mobile/05-cases/case-app-performance.md +500 -0
- package/mobile/05-cases/case-app-startup-optimization.md +218 -0
- package/mobile/06-glossary/mobile-glossary.md +484 -0
- package/observability/01-standards/observability-standards.md +103 -0
- package/observability/02-playbooks/prometheus-grafana-playbook.md +135 -0
- package/observability/02-playbooks/structured-logging-playbook.md +73 -0
- package/observability/03-checklists/observability-checklist.md +54 -0
- package/observability/04-antipatterns/observability-antipatterns.md +106 -0
- package/operations/01-standards/prometheus-monitoring-complete.md +1578 -0
- package/operations/02-playbooks/capacity-planning-playbook.md +620 -0
- package/operations/03-checklists/production-launch-checklist.md +365 -0
- package/operations/04-antipatterns/operations-antipatterns.md +664 -0
- package/operations/05-cases/case-sre-practices.md +581 -0
- package/operations/06-glossary/operations-glossary.md +120 -0
- package/operations/aiops-anomaly-detection.md +758 -0
- package/operations/capacity-planning.md +1061 -0
- package/operations/chaos-engineering.md +659 -0
- package/operations/incident-command-system.md +38 -0
- package/operations/observability-complete.md +442 -0
- package/operations/slo-sli-playbook.md +517 -0
- package/operations/sre-operations-deep-dive.md +39 -0
- package/package.json +8 -0
- package/performance/01-standards/performance-and-scalability.md +80 -0
- package/performance/01-standards/performance-standards.md +156 -0
- package/performance/02-playbooks/query-optimization-playbook.md +103 -0
- package/performance/03-checklists/performance-checklist.md +56 -0
- package/performance/04-antipatterns/performance-antipatterns.md +146 -0
- package/product/01-standards/product-management-complete.md +285 -0
- package/product/02-playbooks/feature-launch-playbook.md +207 -0
- package/product/02-playbooks/user-research-playbook.md +532 -0
- package/product/03-checklists/feature-launch-checklist.md +275 -0
- package/product/04-antipatterns/product-antipatterns.md +355 -0
- package/product/05-cases/case-mvp-to-scale.md +384 -0
- package/product/06-glossary/product-glossary.md +462 -0
- package/product/feature-prioritization-framework.md +40 -0
- package/product/kpi-and-metric-tree.md +37 -0
- package/product/product-discovery-and-prd-deep-dive.md +41 -0
- package/quantum/01-standards/quantum-complete.md +1186 -0
- package/security/01-standards/api-security-complete.md +511 -0
- package/security/01-standards/container-runtime-security.md +574 -0
- package/security/01-standards/data-protection-gdpr.md +543 -0
- package/security/01-standards/owasp-top10-complete.md +1890 -0
- package/security/01-standards/secure-coding-baseline.md +90 -0
- package/security/01-standards/supply-chain-security.md +441 -0
- package/security/01-standards/web-security-checklist.md +108 -0
- package/security/01-standards/zero-trust-architecture.md +521 -0
- package/security/02-playbooks/auth-sso-playbook.md +166 -0
- package/security/02-playbooks/incident-response-security-playbook.md +588 -0
- package/security/02-playbooks/owasp-api-security-playbook.md +129 -0
- package/security/02-playbooks/payment-integration-playbook.md +119 -0
- package/security/02-playbooks/penetration-testing-playbook.md +517 -0
- package/security/03-checklists/security-audit-checklist.md +356 -0
- package/security/04-antipatterns/security-coding-antipatterns.md +580 -0
- package/security/05-cases/case-log4shell-incident.md +537 -0
- package/security/05-cases/case-major-breaches.md +468 -0
- package/security/06-glossary/security-glossary.md +212 -0
- package/security/compliance-automation.md +993 -0
- package/security/container-security.md +680 -0
- package/security/devsecops-complete.md +426 -0
- package/security/sast-dast-sca.md +775 -0
- package/security/secrets-management.md +594 -0
- package/security/security-architecture-deep-dive.md +37 -0
- package/security/threat-modeling-stride-playbook.md +40 -0
- package/seed-templates/auth-system.md +59 -0
- package/seed-templates/blog-content.md +94 -0
- package/seed-templates/dashboard.md +89 -0
- package/seed-templates/docs-site.md +73 -0
- package/seed-templates/e-commerce.md +50 -0
- package/seed-templates/saas-landing.md +92 -0
- package/seed-templates/settings-page.md +51 -0
- package/testing/01-standards/test-strategy-and-layering.md +83 -0
- package/testing/01-standards/testing-strategy-complete.md +422 -0
- package/testing/01-standards/unit-testing-best-practices.md +118 -0
- package/testing/02-playbooks/e2e-testing-playbook.md +988 -0
- package/testing/02-playbooks/testing-strategy-playbook.md +126 -0
- package/testing/03-checklists/test-strategy-checklist.md +208 -0
- package/testing/04-antipatterns/testing-antipatterns.md +718 -0
- package/testing/05-cases/case-testing-transformation.md +300 -0
- package/testing/06-glossary/testing-glossary.md +110 -0
- package/testing/risk-based-test-matrix.md +36 -0
- package/testing/testing-strategy-deep-dive.md +37 -0
|
@@ -0,0 +1,1765 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: django-complete
|
|
3
|
+
title: Django 完整指南
|
|
4
|
+
domain: backend
|
|
5
|
+
category: 01-standards
|
|
6
|
+
difficulty: intermediate
|
|
7
|
+
tags: [backend, complete, django, framework, rest, 中间件, 数据库迁移, 概述]
|
|
8
|
+
quality_score: 70
|
|
9
|
+
last_updated: 2026-06-15
|
|
10
|
+
---
|
|
11
|
+
# Django 完整指南
|
|
12
|
+
|
|
13
|
+
## 概述
|
|
14
|
+
|
|
15
|
+
Django 是 Python 生态中最成熟的全栈 Web 框架,遵循 MTV(Model-Template-View)模式,内置 ORM、认证系统、后台管理、表单处理、国际化等企业级功能。Django 秉承"batteries included"哲学,适用于从原型到大规模生产的全生命周期开发。
|
|
16
|
+
|
|
17
|
+
### 核心特性
|
|
18
|
+
|
|
19
|
+
- **ORM**: 强大的对象关系映射,支持多种数据库后端
|
|
20
|
+
- **Admin**: 自动生成的后台管理界面
|
|
21
|
+
- **认证系统**: 内置用户认证、权限、组管理
|
|
22
|
+
- **迁移系统**: 数据库 schema 版本控制
|
|
23
|
+
- **安全**: 内置 CSRF/XSS/SQL 注入/Clickjacking 防护
|
|
24
|
+
- **缓存框架**: 多级缓存策略,支持 Redis/Memcached
|
|
25
|
+
- **信号系统**: 松耦合的事件驱动扩展机制
|
|
26
|
+
- **中间件**: 请求/响应处理管线
|
|
27
|
+
|
|
28
|
+
### 为什么选择 Django?
|
|
29
|
+
|
|
30
|
+
- 成熟稳定,社区活跃(GitHub 70k+ stars)
|
|
31
|
+
- 文档质量业界公认一流
|
|
32
|
+
- Instagram/Pinterest/Disqus/Mozilla 等大规模验证
|
|
33
|
+
- 丰富的第三方生态(DRF/Celery/Channels/django-allauth 等)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 项目结构最佳实践
|
|
38
|
+
|
|
39
|
+
### 标准项目布局
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
project_root/
|
|
43
|
+
├── manage.py
|
|
44
|
+
├── requirements/
|
|
45
|
+
│ ├── base.txt
|
|
46
|
+
│ ├── dev.txt
|
|
47
|
+
│ └── prod.txt
|
|
48
|
+
├── config/ # 项目配置包(推荐重命名)
|
|
49
|
+
│ ├── __init__.py
|
|
50
|
+
│ ├── settings/
|
|
51
|
+
│ │ ├── __init__.py
|
|
52
|
+
│ │ ├── base.py # 公共配置
|
|
53
|
+
│ │ ├── dev.py # 开发环境
|
|
54
|
+
│ │ ├── staging.py # 预发布环境
|
|
55
|
+
│ │ └── prod.py # 生产环境
|
|
56
|
+
│ ├── urls.py
|
|
57
|
+
│ ├── wsgi.py
|
|
58
|
+
│ └── asgi.py
|
|
59
|
+
├── apps/
|
|
60
|
+
│ ├── users/
|
|
61
|
+
│ │ ├── __init__.py
|
|
62
|
+
│ │ ├── admin.py
|
|
63
|
+
│ │ ├── apps.py
|
|
64
|
+
│ │ ├── models.py
|
|
65
|
+
│ │ ├── managers.py # 自定义 Manager
|
|
66
|
+
│ │ ├── serializers.py
|
|
67
|
+
│ │ ├── views.py
|
|
68
|
+
│ │ ├── urls.py
|
|
69
|
+
│ │ ├── signals.py
|
|
70
|
+
│ │ ├── tasks.py # Celery 任务
|
|
71
|
+
│ │ ├── permissions.py
|
|
72
|
+
│ │ ├── tests/
|
|
73
|
+
│ │ │ ├── __init__.py
|
|
74
|
+
│ │ │ ├── test_models.py
|
|
75
|
+
│ │ │ ├── test_views.py
|
|
76
|
+
│ │ │ └── factories.py # Factory Boy
|
|
77
|
+
│ │ └── migrations/
|
|
78
|
+
│ └── orders/
|
|
79
|
+
│ └── ...
|
|
80
|
+
├── templates/
|
|
81
|
+
├── static/
|
|
82
|
+
├── media/
|
|
83
|
+
├── locale/
|
|
84
|
+
├── docker/
|
|
85
|
+
│ ├── Dockerfile
|
|
86
|
+
│ └── docker-compose.yml
|
|
87
|
+
└── docs/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 设置拆分最佳实践
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# config/settings/base.py
|
|
94
|
+
import os
|
|
95
|
+
from pathlib import Path
|
|
96
|
+
|
|
97
|
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
98
|
+
|
|
99
|
+
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
|
|
100
|
+
|
|
101
|
+
INSTALLED_APPS = [
|
|
102
|
+
"django.contrib.admin",
|
|
103
|
+
"django.contrib.auth",
|
|
104
|
+
"django.contrib.contenttypes",
|
|
105
|
+
"django.contrib.sessions",
|
|
106
|
+
"django.contrib.messages",
|
|
107
|
+
"django.contrib.staticfiles",
|
|
108
|
+
# 第三方
|
|
109
|
+
"rest_framework",
|
|
110
|
+
"django_filters",
|
|
111
|
+
"corsheaders",
|
|
112
|
+
# 本地
|
|
113
|
+
"apps.users",
|
|
114
|
+
"apps.orders",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
AUTH_USER_MODEL = "users.User"
|
|
118
|
+
|
|
119
|
+
# config/settings/dev.py
|
|
120
|
+
from .base import * # noqa: F401,F403
|
|
121
|
+
|
|
122
|
+
DEBUG = True
|
|
123
|
+
ALLOWED_HOSTS = ["*"]
|
|
124
|
+
DATABASES = {
|
|
125
|
+
"default": {
|
|
126
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
127
|
+
"NAME": "myproject_dev",
|
|
128
|
+
"HOST": "localhost",
|
|
129
|
+
"PORT": "5432",
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# config/settings/prod.py
|
|
134
|
+
from .base import * # noqa: F401,F403
|
|
135
|
+
|
|
136
|
+
DEBUG = False
|
|
137
|
+
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
|
|
138
|
+
DATABASES = {
|
|
139
|
+
"default": {
|
|
140
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
141
|
+
"NAME": os.environ["DB_NAME"],
|
|
142
|
+
"USER": os.environ["DB_USER"],
|
|
143
|
+
"PASSWORD": os.environ["DB_PASSWORD"],
|
|
144
|
+
"HOST": os.environ["DB_HOST"],
|
|
145
|
+
"PORT": os.environ.get("DB_PORT", "5432"),
|
|
146
|
+
"CONN_MAX_AGE": 600,
|
|
147
|
+
"OPTIONS": {
|
|
148
|
+
"connect_timeout": 10,
|
|
149
|
+
"options": "-c statement_timeout=30000",
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## ORM 高级用法
|
|
158
|
+
|
|
159
|
+
### 模型定义
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from django.db import models
|
|
163
|
+
from django.utils import timezone
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TimeStampedModel(models.Model):
|
|
167
|
+
"""可复用的时间戳抽象基类"""
|
|
168
|
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
169
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
170
|
+
|
|
171
|
+
class Meta:
|
|
172
|
+
abstract = True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Category(models.Model):
|
|
176
|
+
name = models.CharField(max_length=100, unique=True)
|
|
177
|
+
slug = models.SlugField(unique=True)
|
|
178
|
+
parent = models.ForeignKey(
|
|
179
|
+
"self", null=True, blank=True,
|
|
180
|
+
on_delete=models.CASCADE,
|
|
181
|
+
related_name="children",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
class Meta:
|
|
185
|
+
verbose_name_plural = "Categories"
|
|
186
|
+
ordering = ["name"]
|
|
187
|
+
indexes = [
|
|
188
|
+
models.Index(fields=["slug"]),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
def __str__(self):
|
|
192
|
+
return self.name
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class Article(TimeStampedModel):
|
|
196
|
+
class Status(models.TextChoices):
|
|
197
|
+
DRAFT = "draft", "Draft"
|
|
198
|
+
PUBLISHED = "published", "Published"
|
|
199
|
+
ARCHIVED = "archived", "Archived"
|
|
200
|
+
|
|
201
|
+
title = models.CharField(max_length=200)
|
|
202
|
+
slug = models.SlugField(max_length=200, unique_for_date="publish_date")
|
|
203
|
+
author = models.ForeignKey(
|
|
204
|
+
"users.User",
|
|
205
|
+
on_delete=models.CASCADE,
|
|
206
|
+
related_name="articles",
|
|
207
|
+
)
|
|
208
|
+
category = models.ForeignKey(
|
|
209
|
+
Category, on_delete=models.SET_NULL,
|
|
210
|
+
null=True, related_name="articles",
|
|
211
|
+
)
|
|
212
|
+
body = models.TextField()
|
|
213
|
+
status = models.CharField(
|
|
214
|
+
max_length=10,
|
|
215
|
+
choices=Status.choices,
|
|
216
|
+
default=Status.DRAFT,
|
|
217
|
+
db_index=True,
|
|
218
|
+
)
|
|
219
|
+
publish_date = models.DateTimeField(default=timezone.now)
|
|
220
|
+
tags = models.ManyToManyField("Tag", blank=True, related_name="articles")
|
|
221
|
+
view_count = models.PositiveIntegerField(default=0)
|
|
222
|
+
|
|
223
|
+
class Meta:
|
|
224
|
+
ordering = ["-publish_date"]
|
|
225
|
+
indexes = [
|
|
226
|
+
models.Index(fields=["-publish_date", "status"]),
|
|
227
|
+
models.Index(fields=["author", "status"]),
|
|
228
|
+
]
|
|
229
|
+
constraints = [
|
|
230
|
+
models.CheckConstraint(
|
|
231
|
+
check=models.Q(view_count__gte=0),
|
|
232
|
+
name="view_count_non_negative",
|
|
233
|
+
),
|
|
234
|
+
]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### QuerySet 高级查询
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from django.db.models import Q, F, Count, Avg, Sum, Subquery, OuterRef, Exists
|
|
241
|
+
|
|
242
|
+
# F 表达式 —— 引用字段值进行数据库级操作
|
|
243
|
+
Article.objects.filter(updated_at__gt=F("created_at"))
|
|
244
|
+
Article.objects.update(view_count=F("view_count") + 1)
|
|
245
|
+
|
|
246
|
+
# Q 对象 —— 构建复杂查询条件
|
|
247
|
+
Article.objects.filter(
|
|
248
|
+
Q(status="published") & (Q(title__icontains="django") | Q(body__icontains="django"))
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# 排除条件
|
|
252
|
+
Article.objects.exclude(
|
|
253
|
+
Q(status="draft") | Q(author__is_active=False)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# annotate —— 为每行附加聚合计算值
|
|
257
|
+
authors_with_stats = User.objects.annotate(
|
|
258
|
+
article_count=Count("articles"),
|
|
259
|
+
avg_views=Avg("articles__view_count"),
|
|
260
|
+
total_views=Sum("articles__view_count"),
|
|
261
|
+
).filter(article_count__gt=0).order_by("-total_views")
|
|
262
|
+
|
|
263
|
+
# aggregate —— 全表聚合
|
|
264
|
+
from django.db.models import Max, Min
|
|
265
|
+
stats = Article.objects.filter(status="published").aggregate(
|
|
266
|
+
total=Count("id"),
|
|
267
|
+
avg_views=Avg("view_count"),
|
|
268
|
+
max_views=Max("view_count"),
|
|
269
|
+
min_views=Min("view_count"),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Subquery —— 子查询
|
|
273
|
+
newest_article = Article.objects.filter(
|
|
274
|
+
author=OuterRef("pk")
|
|
275
|
+
).order_by("-publish_date")
|
|
276
|
+
|
|
277
|
+
users_with_latest = User.objects.annotate(
|
|
278
|
+
latest_article_title=Subquery(newest_article.values("title")[:1]),
|
|
279
|
+
latest_article_date=Subquery(newest_article.values("publish_date")[:1]),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Exists —— 高效存在性检查
|
|
283
|
+
active_authors = User.objects.filter(
|
|
284
|
+
Exists(Article.objects.filter(author=OuterRef("pk"), status="published"))
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Prefetch 与 select_related
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from django.db.models import Prefetch
|
|
292
|
+
|
|
293
|
+
# select_related —— ForeignKey / OneToOne,单次 JOIN
|
|
294
|
+
articles = Article.objects.select_related("author", "category").all()
|
|
295
|
+
|
|
296
|
+
# prefetch_related —— ManyToMany / 反向 FK,独立查询后合并
|
|
297
|
+
articles = Article.objects.prefetch_related("tags").all()
|
|
298
|
+
|
|
299
|
+
# Prefetch 对象 —— 自定义预取 QuerySet
|
|
300
|
+
published_articles = Article.objects.filter(status="published")
|
|
301
|
+
users = User.objects.prefetch_related(
|
|
302
|
+
Prefetch(
|
|
303
|
+
"articles",
|
|
304
|
+
queryset=published_articles.select_related("category"),
|
|
305
|
+
to_attr="published_articles", # 结果存到属性而非 Manager
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# 嵌套预取
|
|
310
|
+
categories = Category.objects.prefetch_related(
|
|
311
|
+
Prefetch(
|
|
312
|
+
"articles",
|
|
313
|
+
queryset=Article.objects.select_related("author").prefetch_related("tags"),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 自定义 Manager 和 QuerySet
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
class PublishedQuerySet(models.QuerySet):
|
|
322
|
+
def published(self):
|
|
323
|
+
return self.filter(status="published", publish_date__lte=timezone.now())
|
|
324
|
+
|
|
325
|
+
def by_author(self, user):
|
|
326
|
+
return self.filter(author=user)
|
|
327
|
+
|
|
328
|
+
def popular(self, min_views=100):
|
|
329
|
+
return self.filter(view_count__gte=min_views)
|
|
330
|
+
|
|
331
|
+
def with_stats(self):
|
|
332
|
+
return self.annotate(
|
|
333
|
+
comment_count=Count("comments"),
|
|
334
|
+
avg_rating=Avg("ratings__score"),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class PublishedManager(models.Manager):
|
|
339
|
+
def get_queryset(self):
|
|
340
|
+
return PublishedQuerySet(self.model, using=self._db).published()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Article(TimeStampedModel):
|
|
344
|
+
# ...字段定义...
|
|
345
|
+
objects = models.Manager() # 默认 Manager
|
|
346
|
+
published = PublishedManager() # 自定义 Manager
|
|
347
|
+
|
|
348
|
+
class Meta:
|
|
349
|
+
default_manager_name = "objects"
|
|
350
|
+
|
|
351
|
+
# 链式调用
|
|
352
|
+
Article.published.by_author(user).popular().with_stats()
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 数据库迁移
|
|
358
|
+
|
|
359
|
+
### 迁移管理
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# 生成迁移文件
|
|
363
|
+
python manage.py makemigrations
|
|
364
|
+
|
|
365
|
+
# 查看迁移 SQL(不执行)
|
|
366
|
+
python manage.py sqlmigrate myapp 0001
|
|
367
|
+
|
|
368
|
+
# 执行迁移
|
|
369
|
+
python manage.py migrate
|
|
370
|
+
|
|
371
|
+
# 查看迁移状态
|
|
372
|
+
python manage.py showmigrations
|
|
373
|
+
|
|
374
|
+
# 回滚到指定迁移
|
|
375
|
+
python manage.py migrate myapp 0003
|
|
376
|
+
|
|
377
|
+
# 生成空迁移(用于数据迁移)
|
|
378
|
+
python manage.py makemigrations myapp --empty -n populate_slugs
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 数据迁移
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
from django.db import migrations
|
|
385
|
+
|
|
386
|
+
def populate_slugs(apps, schema_editor):
|
|
387
|
+
from django.utils.text import slugify
|
|
388
|
+
Article = apps.get_model("myapp", "Article")
|
|
389
|
+
for article in Article.objects.filter(slug=""):
|
|
390
|
+
article.slug = slugify(article.title)
|
|
391
|
+
article.save(update_fields=["slug"])
|
|
392
|
+
|
|
393
|
+
def reverse_slugs(apps, schema_editor):
|
|
394
|
+
pass # 反向迁移通常不需要操作
|
|
395
|
+
|
|
396
|
+
class Migration(migrations.Migration):
|
|
397
|
+
dependencies = [("myapp", "0005_article_slug")]
|
|
398
|
+
|
|
399
|
+
operations = [
|
|
400
|
+
migrations.RunPython(populate_slugs, reverse_slugs),
|
|
401
|
+
]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### 迁移最佳实践
|
|
405
|
+
|
|
406
|
+
- 每次 `makemigrations` 只修改一个 app
|
|
407
|
+
- 数据迁移和 schema 迁移分开写
|
|
408
|
+
- 大表添加列时使用 `db_default`(Django 5.0+)或分步迁移
|
|
409
|
+
- 避免在迁移中直接 import Model,使用 `apps.get_model()`
|
|
410
|
+
- 生产环境迁移前先在 staging 测试
|
|
411
|
+
- 使用 `--check` 在 CI 中验证迁移文件是否最新
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## 认证与权限
|
|
416
|
+
|
|
417
|
+
### 自定义 User 模型
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
from django.contrib.auth.models import AbstractUser, BaseUserManager
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class UserManager(BaseUserManager):
|
|
424
|
+
def create_user(self, email, password=None, **extra_fields):
|
|
425
|
+
if not email:
|
|
426
|
+
raise ValueError("Email is required")
|
|
427
|
+
email = self.normalize_email(email)
|
|
428
|
+
user = self.model(email=email, **extra_fields)
|
|
429
|
+
user.set_password(password)
|
|
430
|
+
user.save(using=self._db)
|
|
431
|
+
return user
|
|
432
|
+
|
|
433
|
+
def create_superuser(self, email, password=None, **extra_fields):
|
|
434
|
+
extra_fields.setdefault("is_staff", True)
|
|
435
|
+
extra_fields.setdefault("is_superuser", True)
|
|
436
|
+
return self.create_user(email, password, **extra_fields)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class User(AbstractUser):
|
|
440
|
+
username = None # 移除 username 字段
|
|
441
|
+
email = models.EmailField(unique=True)
|
|
442
|
+
phone = models.CharField(max_length=20, blank=True)
|
|
443
|
+
avatar = models.ImageField(upload_to="avatars/", blank=True)
|
|
444
|
+
|
|
445
|
+
USERNAME_FIELD = "email"
|
|
446
|
+
REQUIRED_FIELDS = []
|
|
447
|
+
|
|
448
|
+
objects = UserManager()
|
|
449
|
+
|
|
450
|
+
def __str__(self):
|
|
451
|
+
return self.email
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Groups 和 Permissions
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
from django.contrib.auth.models import Group, Permission
|
|
458
|
+
from django.contrib.contenttypes.models import ContentType
|
|
459
|
+
|
|
460
|
+
# 创建自定义权限
|
|
461
|
+
class Article(models.Model):
|
|
462
|
+
class Meta:
|
|
463
|
+
permissions = [
|
|
464
|
+
("publish_article", "Can publish article"),
|
|
465
|
+
("feature_article", "Can feature article on homepage"),
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
# 程序化管理权限
|
|
469
|
+
def setup_groups():
|
|
470
|
+
editors, _ = Group.objects.get_or_create(name="Editors")
|
|
471
|
+
content_type = ContentType.objects.get_for_model(Article)
|
|
472
|
+
|
|
473
|
+
publish_perm = Permission.objects.get(
|
|
474
|
+
codename="publish_article", content_type=content_type
|
|
475
|
+
)
|
|
476
|
+
editors.permissions.add(publish_perm)
|
|
477
|
+
|
|
478
|
+
# 检查权限
|
|
479
|
+
user.has_perm("myapp.publish_article")
|
|
480
|
+
user.has_perms(["myapp.publish_article", "myapp.change_article"])
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### JWT 认证(djangorestframework-simplejwt)
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
# settings.py
|
|
487
|
+
from datetime import timedelta
|
|
488
|
+
|
|
489
|
+
INSTALLED_APPS += ["rest_framework_simplejwt"]
|
|
490
|
+
|
|
491
|
+
REST_FRAMEWORK = {
|
|
492
|
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
493
|
+
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
|
494
|
+
],
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
SIMPLE_JWT = {
|
|
498
|
+
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
|
|
499
|
+
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
|
|
500
|
+
"ROTATE_REFRESH_TOKENS": True,
|
|
501
|
+
"BLACKLIST_AFTER_ROTATION": True,
|
|
502
|
+
"AUTH_HEADER_TYPES": ("Bearer",),
|
|
503
|
+
"TOKEN_OBTAIN_SERIALIZER": "apps.users.serializers.CustomTokenObtainPairSerializer",
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# urls.py
|
|
507
|
+
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
|
|
508
|
+
|
|
509
|
+
urlpatterns = [
|
|
510
|
+
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
|
|
511
|
+
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
# 自定义 Token 载荷
|
|
515
|
+
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
|
516
|
+
|
|
517
|
+
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
|
|
518
|
+
@classmethod
|
|
519
|
+
def get_token(cls, user):
|
|
520
|
+
token = super().get_token(user)
|
|
521
|
+
token["email"] = user.email
|
|
522
|
+
token["is_staff"] = user.is_staff
|
|
523
|
+
return token
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Django REST Framework
|
|
529
|
+
|
|
530
|
+
### Serializer
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
from rest_framework import serializers
|
|
534
|
+
|
|
535
|
+
class ArticleSerializer(serializers.ModelSerializer):
|
|
536
|
+
author_name = serializers.CharField(source="author.get_full_name", read_only=True)
|
|
537
|
+
category_name = serializers.CharField(source="category.name", read_only=True)
|
|
538
|
+
comment_count = serializers.IntegerField(read_only=True)
|
|
539
|
+
|
|
540
|
+
class Meta:
|
|
541
|
+
model = Article
|
|
542
|
+
fields = [
|
|
543
|
+
"id", "title", "slug", "body", "status",
|
|
544
|
+
"author", "author_name",
|
|
545
|
+
"category", "category_name",
|
|
546
|
+
"publish_date", "view_count", "comment_count",
|
|
547
|
+
"created_at", "updated_at",
|
|
548
|
+
]
|
|
549
|
+
read_only_fields = ["author", "view_count"]
|
|
550
|
+
extra_kwargs = {
|
|
551
|
+
"body": {"min_length": 50},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
def validate_title(self, value):
|
|
555
|
+
if Article.objects.filter(title=value).exclude(pk=self.instance and self.instance.pk).exists():
|
|
556
|
+
raise serializers.ValidationError("Title already exists.")
|
|
557
|
+
return value
|
|
558
|
+
|
|
559
|
+
def validate(self, data):
|
|
560
|
+
if data.get("status") == "published" and not data.get("category"):
|
|
561
|
+
raise serializers.ValidationError(
|
|
562
|
+
{"category": "Published articles must have a category."}
|
|
563
|
+
)
|
|
564
|
+
return data
|
|
565
|
+
|
|
566
|
+
def create(self, validated_data):
|
|
567
|
+
validated_data["author"] = self.context["request"].user
|
|
568
|
+
return super().create(validated_data)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class ArticleListSerializer(serializers.ModelSerializer):
|
|
572
|
+
"""列表专用轻量 Serializer,减少数据传输"""
|
|
573
|
+
class Meta:
|
|
574
|
+
model = Article
|
|
575
|
+
fields = ["id", "title", "slug", "status", "publish_date", "view_count"]
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### ViewSet
|
|
579
|
+
|
|
580
|
+
```python
|
|
581
|
+
from rest_framework import viewsets, status
|
|
582
|
+
from rest_framework.decorators import action
|
|
583
|
+
from rest_framework.response import Response
|
|
584
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
|
585
|
+
from rest_framework.filters import SearchFilter, OrderingFilter
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
589
|
+
queryset = Article.objects.select_related("author", "category")
|
|
590
|
+
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
|
591
|
+
filterset_fields = ["status", "category", "author"]
|
|
592
|
+
search_fields = ["title", "body"]
|
|
593
|
+
ordering_fields = ["publish_date", "view_count"]
|
|
594
|
+
ordering = ["-publish_date"]
|
|
595
|
+
|
|
596
|
+
def get_serializer_class(self):
|
|
597
|
+
if self.action == "list":
|
|
598
|
+
return ArticleListSerializer
|
|
599
|
+
return ArticleSerializer
|
|
600
|
+
|
|
601
|
+
def get_queryset(self):
|
|
602
|
+
qs = super().get_queryset()
|
|
603
|
+
if self.action == "list":
|
|
604
|
+
qs = qs.annotate(comment_count=Count("comments"))
|
|
605
|
+
return qs
|
|
606
|
+
|
|
607
|
+
@action(detail=True, methods=["post"])
|
|
608
|
+
def publish(self, request, pk=None):
|
|
609
|
+
article = self.get_object()
|
|
610
|
+
article.status = "published"
|
|
611
|
+
article.publish_date = timezone.now()
|
|
612
|
+
article.save(update_fields=["status", "publish_date"])
|
|
613
|
+
return Response({"status": "published"})
|
|
614
|
+
|
|
615
|
+
@action(detail=False, methods=["get"])
|
|
616
|
+
def my_articles(self, request):
|
|
617
|
+
qs = self.get_queryset().filter(author=request.user)
|
|
618
|
+
page = self.paginate_queryset(qs)
|
|
619
|
+
if page is not None:
|
|
620
|
+
serializer = self.get_serializer(page, many=True)
|
|
621
|
+
return self.get_paginated_response(serializer.data)
|
|
622
|
+
serializer = self.get_serializer(qs, many=True)
|
|
623
|
+
return Response(serializer.data)
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Permission
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class IsAuthorOrReadOnly(BasePermission):
|
|
633
|
+
def has_object_permission(self, request, view, obj):
|
|
634
|
+
if request.method in SAFE_METHODS:
|
|
635
|
+
return True
|
|
636
|
+
return obj.author == request.user
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class IsAdminOrEditor(BasePermission):
|
|
640
|
+
def has_permission(self, request, view):
|
|
641
|
+
return request.user.is_staff or request.user.groups.filter(name="Editors").exists()
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# 在 ViewSet 中组合使用
|
|
645
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
646
|
+
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
|
|
647
|
+
|
|
648
|
+
def get_permissions(self):
|
|
649
|
+
if self.action in ["publish", "destroy"]:
|
|
650
|
+
return [IsAuthenticated(), IsAdminOrEditor()]
|
|
651
|
+
return super().get_permissions()
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Pagination
|
|
655
|
+
|
|
656
|
+
```python
|
|
657
|
+
# settings.py
|
|
658
|
+
REST_FRAMEWORK = {
|
|
659
|
+
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
660
|
+
"PAGE_SIZE": 20,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
# 自定义分页
|
|
664
|
+
from rest_framework.pagination import CursorPagination
|
|
665
|
+
|
|
666
|
+
class ArticleCursorPagination(CursorPagination):
|
|
667
|
+
page_size = 20
|
|
668
|
+
ordering = "-publish_date"
|
|
669
|
+
cursor_query_param = "cursor"
|
|
670
|
+
|
|
671
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
672
|
+
pagination_class = ArticleCursorPagination
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Throttling
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
# settings.py
|
|
679
|
+
REST_FRAMEWORK = {
|
|
680
|
+
"DEFAULT_THROTTLE_CLASSES": [
|
|
681
|
+
"rest_framework.throttling.AnonRateThrottle",
|
|
682
|
+
"rest_framework.throttling.UserRateThrottle",
|
|
683
|
+
],
|
|
684
|
+
"DEFAULT_THROTTLE_RATES": {
|
|
685
|
+
"anon": "100/hour",
|
|
686
|
+
"user": "1000/hour",
|
|
687
|
+
"burst": "60/minute",
|
|
688
|
+
},
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
from rest_framework.throttling import UserRateThrottle
|
|
692
|
+
|
|
693
|
+
class BurstRateThrottle(UserRateThrottle):
|
|
694
|
+
scope = "burst"
|
|
695
|
+
|
|
696
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
697
|
+
throttle_classes = [BurstRateThrottle]
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## 中间件
|
|
703
|
+
|
|
704
|
+
### 自定义中间件
|
|
705
|
+
|
|
706
|
+
```python
|
|
707
|
+
import time
|
|
708
|
+
import logging
|
|
709
|
+
|
|
710
|
+
logger = logging.getLogger(__name__)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class RequestTimingMiddleware:
|
|
714
|
+
"""记录每次请求的耗时"""
|
|
715
|
+
def __init__(self, get_response):
|
|
716
|
+
self.get_response = get_response
|
|
717
|
+
|
|
718
|
+
def __call__(self, request):
|
|
719
|
+
start = time.monotonic()
|
|
720
|
+
response = self.get_response(request)
|
|
721
|
+
duration = time.monotonic() - start
|
|
722
|
+
response["X-Request-Duration"] = f"{duration:.4f}s"
|
|
723
|
+
if duration > 1.0:
|
|
724
|
+
logger.warning(
|
|
725
|
+
"Slow request: %s %s took %.2fs",
|
|
726
|
+
request.method, request.path, duration,
|
|
727
|
+
)
|
|
728
|
+
return response
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
class RequestIDMiddleware:
|
|
732
|
+
"""为每次请求添加唯一 ID,方便日志追踪"""
|
|
733
|
+
def __init__(self, get_response):
|
|
734
|
+
self.get_response = get_response
|
|
735
|
+
|
|
736
|
+
def __call__(self, request):
|
|
737
|
+
import uuid
|
|
738
|
+
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
|
|
739
|
+
request.request_id = request_id
|
|
740
|
+
response = self.get_response(request)
|
|
741
|
+
response["X-Request-ID"] = request_id
|
|
742
|
+
return response
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
# settings.py
|
|
746
|
+
MIDDLEWARE = [
|
|
747
|
+
"django.middleware.security.SecurityMiddleware",
|
|
748
|
+
"corsheaders.middleware.CorsMiddleware",
|
|
749
|
+
"apps.core.middleware.RequestIDMiddleware",
|
|
750
|
+
"apps.core.middleware.RequestTimingMiddleware",
|
|
751
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
752
|
+
"django.middleware.common.CommonMiddleware",
|
|
753
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
754
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
755
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
756
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
757
|
+
]
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## 信号系统
|
|
763
|
+
|
|
764
|
+
### 信号定义与使用
|
|
765
|
+
|
|
766
|
+
```python
|
|
767
|
+
# apps/users/signals.py
|
|
768
|
+
from django.db.models.signals import post_save, pre_save
|
|
769
|
+
from django.dispatch import receiver
|
|
770
|
+
from django.conf import settings
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
|
|
774
|
+
def create_user_profile(sender, instance, created, **kwargs):
|
|
775
|
+
if created:
|
|
776
|
+
Profile.objects.create(user=instance)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@receiver(pre_save, sender="articles.Article")
|
|
780
|
+
def auto_slug(sender, instance, **kwargs):
|
|
781
|
+
if not instance.slug:
|
|
782
|
+
from django.utils.text import slugify
|
|
783
|
+
instance.slug = slugify(instance.title)
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### 信号注册
|
|
787
|
+
|
|
788
|
+
```python
|
|
789
|
+
# apps/users/apps.py
|
|
790
|
+
from django.apps import AppConfig
|
|
791
|
+
|
|
792
|
+
class UsersConfig(AppConfig):
|
|
793
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
794
|
+
name = "apps.users"
|
|
795
|
+
|
|
796
|
+
def ready(self):
|
|
797
|
+
import apps.users.signals # noqa: F401
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### 信号使用原则
|
|
801
|
+
|
|
802
|
+
- 信号适合跨 app 通知(如用户注册后发邮件)
|
|
803
|
+
- 不要用信号做本 app 内的业务逻辑,应放在 Model 方法或 Service 层
|
|
804
|
+
- 信号中避免耗时操作,长任务交给 Celery
|
|
805
|
+
- 测试时可用 `signal.disconnect()` 临时解除
|
|
806
|
+
|
|
807
|
+
---
|
|
808
|
+
|
|
809
|
+
## 缓存策略
|
|
810
|
+
|
|
811
|
+
### Redis 缓存配置
|
|
812
|
+
|
|
813
|
+
```python
|
|
814
|
+
# settings.py
|
|
815
|
+
CACHES = {
|
|
816
|
+
"default": {
|
|
817
|
+
"BACKEND": "django_redis.cache.RedisCache",
|
|
818
|
+
"LOCATION": os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/1"),
|
|
819
|
+
"OPTIONS": {
|
|
820
|
+
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
821
|
+
"SERIALIZER": "django_redis.serializers.json.JSONSerializer",
|
|
822
|
+
"CONNECTION_POOL_KWARGS": {"max_connections": 50},
|
|
823
|
+
"SOCKET_CONNECT_TIMEOUT": 5,
|
|
824
|
+
"SOCKET_TIMEOUT": 5,
|
|
825
|
+
},
|
|
826
|
+
"KEY_PREFIX": "myproject",
|
|
827
|
+
"TIMEOUT": 300, # 默认 5 分钟
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
# 使用 Redis 做 Session 后端
|
|
832
|
+
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
|
833
|
+
SESSION_CACHE_ALIAS = "default"
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Per-View 缓存
|
|
837
|
+
|
|
838
|
+
```python
|
|
839
|
+
from django.views.decorators.cache import cache_page
|
|
840
|
+
from django.utils.decorators import method_decorator
|
|
841
|
+
|
|
842
|
+
# 函数视图
|
|
843
|
+
@cache_page(60 * 15) # 缓存 15 分钟
|
|
844
|
+
def article_list(request):
|
|
845
|
+
...
|
|
846
|
+
|
|
847
|
+
# 类视图
|
|
848
|
+
@method_decorator(cache_page(60 * 15), name="dispatch")
|
|
849
|
+
class ArticleListView(ListView):
|
|
850
|
+
...
|
|
851
|
+
|
|
852
|
+
# DRF ViewSet —— 使用 vary_on_headers 区分用户
|
|
853
|
+
from django.views.decorators.vary import vary_on_headers
|
|
854
|
+
|
|
855
|
+
@method_decorator(cache_page(60 * 5), name="list")
|
|
856
|
+
@method_decorator(vary_on_headers("Authorization"), name="list")
|
|
857
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
858
|
+
...
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Per-Site 缓存
|
|
862
|
+
|
|
863
|
+
```python
|
|
864
|
+
MIDDLEWARE = [
|
|
865
|
+
"django.middleware.cache.UpdateCacheMiddleware", # 放在最前
|
|
866
|
+
# ... 其他中间件 ...
|
|
867
|
+
"django.middleware.cache.FetchFromCacheMiddleware", # 放在最后
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
CACHE_MIDDLEWARE_ALIAS = "default"
|
|
871
|
+
CACHE_MIDDLEWARE_SECONDS = 600
|
|
872
|
+
CACHE_MIDDLEWARE_KEY_PREFIX = "site"
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### 低级缓存 API
|
|
876
|
+
|
|
877
|
+
```python
|
|
878
|
+
from django.core.cache import cache
|
|
879
|
+
|
|
880
|
+
# 基本操作
|
|
881
|
+
cache.set("article:123", article_data, timeout=300)
|
|
882
|
+
data = cache.get("article:123")
|
|
883
|
+
cache.delete("article:123")
|
|
884
|
+
|
|
885
|
+
# get_or_set 模式
|
|
886
|
+
def get_article_stats():
|
|
887
|
+
return Article.objects.aggregate(
|
|
888
|
+
total=Count("id"),
|
|
889
|
+
published=Count("id", filter=Q(status="published")),
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
stats = cache.get_or_set("article_stats", get_article_stats, timeout=600)
|
|
893
|
+
|
|
894
|
+
# 批量操作
|
|
895
|
+
cache.set_many({"key1": "val1", "key2": "val2"}, timeout=300)
|
|
896
|
+
data = cache.get_many(["key1", "key2"])
|
|
897
|
+
|
|
898
|
+
# 原子递增
|
|
899
|
+
cache.set("page_views:123", 0)
|
|
900
|
+
cache.incr("page_views:123")
|
|
901
|
+
|
|
902
|
+
# 缓存版本控制
|
|
903
|
+
cache.set("data", value, version=2)
|
|
904
|
+
cache.incr_version("data")
|
|
905
|
+
|
|
906
|
+
# 缓存失效策略
|
|
907
|
+
def invalidate_article_cache(article_id):
|
|
908
|
+
keys = [
|
|
909
|
+
f"article:{article_id}",
|
|
910
|
+
"article_list",
|
|
911
|
+
"article_stats",
|
|
912
|
+
]
|
|
913
|
+
cache.delete_many(keys)
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
---
|
|
917
|
+
|
|
918
|
+
## Celery 异步任务
|
|
919
|
+
|
|
920
|
+
### 基础配置
|
|
921
|
+
|
|
922
|
+
```python
|
|
923
|
+
# config/celery.py
|
|
924
|
+
import os
|
|
925
|
+
from celery import Celery
|
|
926
|
+
|
|
927
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.prod")
|
|
928
|
+
|
|
929
|
+
app = Celery("myproject")
|
|
930
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
931
|
+
app.autodiscover_tasks()
|
|
932
|
+
|
|
933
|
+
# settings.py
|
|
934
|
+
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://127.0.0.1:6379/0")
|
|
935
|
+
CELERY_RESULT_BACKEND = "django-db" # django-celery-results
|
|
936
|
+
CELERY_ACCEPT_CONTENT = ["json"]
|
|
937
|
+
CELERY_TASK_SERIALIZER = "json"
|
|
938
|
+
CELERY_RESULT_SERIALIZER = "json"
|
|
939
|
+
CELERY_TIMEZONE = "Asia/Shanghai"
|
|
940
|
+
CELERY_TASK_TRACK_STARTED = True
|
|
941
|
+
CELERY_TASK_TIME_LIMIT = 600
|
|
942
|
+
CELERY_TASK_SOFT_TIME_LIMIT = 300
|
|
943
|
+
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000
|
|
944
|
+
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### 任务定义与重试
|
|
948
|
+
|
|
949
|
+
```python
|
|
950
|
+
# apps/notifications/tasks.py
|
|
951
|
+
from celery import shared_task
|
|
952
|
+
from celery.utils.log import get_task_logger
|
|
953
|
+
|
|
954
|
+
logger = get_task_logger(__name__)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@shared_task(
|
|
958
|
+
bind=True,
|
|
959
|
+
max_retries=3,
|
|
960
|
+
default_retry_delay=60,
|
|
961
|
+
autoretry_for=(ConnectionError, TimeoutError),
|
|
962
|
+
retry_backoff=True,
|
|
963
|
+
retry_backoff_max=600,
|
|
964
|
+
retry_jitter=True,
|
|
965
|
+
)
|
|
966
|
+
def send_notification_email(self, user_id, template_name, context):
|
|
967
|
+
try:
|
|
968
|
+
user = User.objects.get(id=user_id)
|
|
969
|
+
send_mail(
|
|
970
|
+
subject=context["subject"],
|
|
971
|
+
message="",
|
|
972
|
+
html_message=render_to_string(template_name, context),
|
|
973
|
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
974
|
+
recipient_list=[user.email],
|
|
975
|
+
)
|
|
976
|
+
logger.info("Email sent to user %s", user_id)
|
|
977
|
+
except User.DoesNotExist:
|
|
978
|
+
logger.error("User %s not found, not retrying", user_id)
|
|
979
|
+
except Exception as exc:
|
|
980
|
+
logger.warning("Email failed for user %s: %s", user_id, exc)
|
|
981
|
+
raise self.retry(exc=exc)
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
### 任务编排
|
|
985
|
+
|
|
986
|
+
```python
|
|
987
|
+
from celery import chain, group, chord
|
|
988
|
+
|
|
989
|
+
# chain —— 串行执行
|
|
990
|
+
result = chain(
|
|
991
|
+
fetch_data.s(url),
|
|
992
|
+
process_data.s(),
|
|
993
|
+
store_results.s(),
|
|
994
|
+
)()
|
|
995
|
+
|
|
996
|
+
# group —— 并行执行
|
|
997
|
+
result = group(
|
|
998
|
+
process_image.s(image_id) for image_id in image_ids
|
|
999
|
+
)()
|
|
1000
|
+
|
|
1001
|
+
# chord —— 并行执行 + 汇总回调
|
|
1002
|
+
result = chord(
|
|
1003
|
+
[analyze_article.s(aid) for aid in article_ids],
|
|
1004
|
+
aggregate_results.s(),
|
|
1005
|
+
)()
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### 任务优先级
|
|
1009
|
+
|
|
1010
|
+
```python
|
|
1011
|
+
# 定义队列
|
|
1012
|
+
CELERY_TASK_ROUTES = {
|
|
1013
|
+
"apps.notifications.tasks.*": {"queue": "notifications"},
|
|
1014
|
+
"apps.analytics.tasks.*": {"queue": "analytics", "priority": 3},
|
|
1015
|
+
"apps.orders.tasks.*": {"queue": "critical", "priority": 9},
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
# 手动指定队列
|
|
1019
|
+
send_notification_email.apply_async(
|
|
1020
|
+
args=[user_id, "welcome.html", ctx],
|
|
1021
|
+
queue="notifications",
|
|
1022
|
+
priority=5,
|
|
1023
|
+
countdown=10, # 延迟 10 秒执行
|
|
1024
|
+
)
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
### Celery Beat 定时任务
|
|
1028
|
+
|
|
1029
|
+
```python
|
|
1030
|
+
from celery.schedules import crontab
|
|
1031
|
+
|
|
1032
|
+
CELERY_BEAT_SCHEDULE = {
|
|
1033
|
+
"cleanup-expired-sessions": {
|
|
1034
|
+
"task": "apps.core.tasks.cleanup_expired_sessions",
|
|
1035
|
+
"schedule": crontab(hour=3, minute=0), # 每天凌晨 3 点
|
|
1036
|
+
},
|
|
1037
|
+
"generate-daily-report": {
|
|
1038
|
+
"task": "apps.analytics.tasks.generate_daily_report",
|
|
1039
|
+
"schedule": crontab(hour=6, minute=30),
|
|
1040
|
+
},
|
|
1041
|
+
"sync-external-data": {
|
|
1042
|
+
"task": "apps.integrations.tasks.sync_external_data",
|
|
1043
|
+
"schedule": 300.0, # 每 5 分钟
|
|
1044
|
+
},
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
## 安全
|
|
1051
|
+
|
|
1052
|
+
### CSRF 防护
|
|
1053
|
+
|
|
1054
|
+
```python
|
|
1055
|
+
# settings.py
|
|
1056
|
+
CSRF_COOKIE_SECURE = True # 仅 HTTPS 传输 CSRF cookie
|
|
1057
|
+
CSRF_COOKIE_HTTPONLY = True # JS 不可读取
|
|
1058
|
+
CSRF_TRUSTED_ORIGINS = [
|
|
1059
|
+
"https://mysite.com",
|
|
1060
|
+
"https://*.mysite.com",
|
|
1061
|
+
]
|
|
1062
|
+
|
|
1063
|
+
# DRF API 中排除 CSRF(JWT 场景)
|
|
1064
|
+
REST_FRAMEWORK = {
|
|
1065
|
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
1066
|
+
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
|
1067
|
+
],
|
|
1068
|
+
# SessionAuthentication 会强制 CSRF,纯 JWT 时不要包含
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### XSS 防护
|
|
1073
|
+
|
|
1074
|
+
```python
|
|
1075
|
+
# Django 模板默认自动转义,确保不要滥用 |safe 过滤器
|
|
1076
|
+
# 在 DRF 中 JSON 响应天然不存在模板 XSS
|
|
1077
|
+
|
|
1078
|
+
# 清理用户输入的 HTML
|
|
1079
|
+
import bleach
|
|
1080
|
+
|
|
1081
|
+
ALLOWED_TAGS = ["p", "b", "i", "u", "a", "ul", "ol", "li", "br", "strong", "em"]
|
|
1082
|
+
ALLOWED_ATTRS = {"a": ["href", "title"]}
|
|
1083
|
+
|
|
1084
|
+
def sanitize_html(raw_html):
|
|
1085
|
+
return bleach.clean(raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, strip=True)
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### SQL 注入防护
|
|
1089
|
+
|
|
1090
|
+
```python
|
|
1091
|
+
# Django ORM 自动参数化,以下是安全的
|
|
1092
|
+
Article.objects.filter(title=user_input)
|
|
1093
|
+
Article.objects.extra(where=["title=%s"], params=[user_input]) # 参数化
|
|
1094
|
+
|
|
1095
|
+
# 危险 —— 永远不要这样做
|
|
1096
|
+
Article.objects.raw(f"SELECT * FROM article WHERE title='{user_input}'")
|
|
1097
|
+
|
|
1098
|
+
# 安全的 raw SQL
|
|
1099
|
+
Article.objects.raw("SELECT * FROM article WHERE title=%s", [user_input])
|
|
1100
|
+
|
|
1101
|
+
# 安全使用 connection.cursor
|
|
1102
|
+
from django.db import connection
|
|
1103
|
+
with connection.cursor() as cursor:
|
|
1104
|
+
cursor.execute("SELECT * FROM article WHERE status=%s", ["published"])
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
### Content Security Policy
|
|
1108
|
+
|
|
1109
|
+
```python
|
|
1110
|
+
# 使用 django-csp
|
|
1111
|
+
# pip install django-csp
|
|
1112
|
+
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]
|
|
1113
|
+
|
|
1114
|
+
CSP_DEFAULT_SRC = ("'self'",)
|
|
1115
|
+
CSP_SCRIPT_SRC = ("'self'", "cdn.jsdelivr.net")
|
|
1116
|
+
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
|
|
1117
|
+
CSP_FONT_SRC = ("'self'", "fonts.gstatic.com")
|
|
1118
|
+
CSP_IMG_SRC = ("'self'", "data:", "cdn.mysite.com")
|
|
1119
|
+
CSP_CONNECT_SRC = ("'self'", "api.mysite.com")
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### 综合安全设置
|
|
1123
|
+
|
|
1124
|
+
```python
|
|
1125
|
+
# settings/prod.py
|
|
1126
|
+
SECURE_HSTS_SECONDS = 31536000
|
|
1127
|
+
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
1128
|
+
SECURE_HSTS_PRELOAD = True
|
|
1129
|
+
SECURE_SSL_REDIRECT = True
|
|
1130
|
+
SECURE_BROWSER_XSS_FILTER = True
|
|
1131
|
+
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
1132
|
+
SESSION_COOKIE_SECURE = True
|
|
1133
|
+
SESSION_COOKIE_HTTPONLY = True
|
|
1134
|
+
SESSION_COOKIE_AGE = 3600 # 1 小时
|
|
1135
|
+
X_FRAME_OPTIONS = "DENY"
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## 测试
|
|
1141
|
+
|
|
1142
|
+
### TestCase 与 APITestCase
|
|
1143
|
+
|
|
1144
|
+
```python
|
|
1145
|
+
from django.test import TestCase, TransactionTestCase
|
|
1146
|
+
from rest_framework.test import APITestCase, APIClient
|
|
1147
|
+
from django.urls import reverse
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
class ArticleModelTest(TestCase):
|
|
1151
|
+
@classmethod
|
|
1152
|
+
def setUpTestData(cls):
|
|
1153
|
+
"""类级别数据准备,整个 TestCase 共享,速度更快"""
|
|
1154
|
+
cls.user = User.objects.create_user(
|
|
1155
|
+
email="test@example.com", password="testpass123"
|
|
1156
|
+
)
|
|
1157
|
+
cls.category = Category.objects.create(name="Tech", slug="tech")
|
|
1158
|
+
|
|
1159
|
+
def test_article_creation(self):
|
|
1160
|
+
article = Article.objects.create(
|
|
1161
|
+
title="Test Article",
|
|
1162
|
+
body="x" * 100,
|
|
1163
|
+
author=self.user,
|
|
1164
|
+
category=self.category,
|
|
1165
|
+
)
|
|
1166
|
+
self.assertEqual(str(article), "Test Article")
|
|
1167
|
+
self.assertEqual(article.status, Article.Status.DRAFT)
|
|
1168
|
+
|
|
1169
|
+
def test_published_manager(self):
|
|
1170
|
+
Article.objects.create(
|
|
1171
|
+
title="Draft", body="x" * 100,
|
|
1172
|
+
author=self.user, status="draft",
|
|
1173
|
+
)
|
|
1174
|
+
Article.objects.create(
|
|
1175
|
+
title="Published", body="x" * 100,
|
|
1176
|
+
author=self.user, status="published",
|
|
1177
|
+
)
|
|
1178
|
+
self.assertEqual(Article.published.count(), 1)
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
class ArticleAPITest(APITestCase):
|
|
1182
|
+
def setUp(self):
|
|
1183
|
+
self.user = User.objects.create_user(
|
|
1184
|
+
email="api@example.com", password="testpass123"
|
|
1185
|
+
)
|
|
1186
|
+
self.client = APIClient()
|
|
1187
|
+
self.client.force_authenticate(user=self.user)
|
|
1188
|
+
|
|
1189
|
+
def test_create_article(self):
|
|
1190
|
+
url = reverse("article-list")
|
|
1191
|
+
data = {
|
|
1192
|
+
"title": "API Article",
|
|
1193
|
+
"body": "x" * 100,
|
|
1194
|
+
"status": "draft",
|
|
1195
|
+
}
|
|
1196
|
+
response = self.client.post(url, data, format="json")
|
|
1197
|
+
self.assertEqual(response.status_code, 201)
|
|
1198
|
+
self.assertEqual(Article.objects.count(), 1)
|
|
1199
|
+
self.assertEqual(Article.objects.first().author, self.user)
|
|
1200
|
+
|
|
1201
|
+
def test_list_articles_pagination(self):
|
|
1202
|
+
for i in range(25):
|
|
1203
|
+
Article.objects.create(
|
|
1204
|
+
title=f"Article {i}", body="x" * 100, author=self.user
|
|
1205
|
+
)
|
|
1206
|
+
response = self.client.get(reverse("article-list"))
|
|
1207
|
+
self.assertEqual(response.status_code, 200)
|
|
1208
|
+
self.assertEqual(len(response.data["results"]), 20)
|
|
1209
|
+
|
|
1210
|
+
def test_unauthorized_delete(self):
|
|
1211
|
+
other_user = User.objects.create_user(email="other@example.com", password="pass")
|
|
1212
|
+
article = Article.objects.create(
|
|
1213
|
+
title="Other", body="x" * 100, author=other_user
|
|
1214
|
+
)
|
|
1215
|
+
url = reverse("article-detail", kwargs={"pk": article.pk})
|
|
1216
|
+
response = self.client.delete(url)
|
|
1217
|
+
self.assertEqual(response.status_code, 403)
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### Factory Boy
|
|
1221
|
+
|
|
1222
|
+
```python
|
|
1223
|
+
# apps/articles/tests/factories.py
|
|
1224
|
+
import factory
|
|
1225
|
+
from factory.django import DjangoModelFactory
|
|
1226
|
+
from apps.users.models import User
|
|
1227
|
+
from apps.articles.models import Article, Category
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
class UserFactory(DjangoModelFactory):
|
|
1231
|
+
class Meta:
|
|
1232
|
+
model = User
|
|
1233
|
+
|
|
1234
|
+
email = factory.Sequence(lambda n: f"user{n}@example.com")
|
|
1235
|
+
password = factory.PostGenerationMethodCall("set_password", "testpass123")
|
|
1236
|
+
is_active = True
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
class CategoryFactory(DjangoModelFactory):
|
|
1240
|
+
class Meta:
|
|
1241
|
+
model = Category
|
|
1242
|
+
|
|
1243
|
+
name = factory.Sequence(lambda n: f"Category {n}")
|
|
1244
|
+
slug = factory.LazyAttribute(lambda o: o.name.lower().replace(" ", "-"))
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
class ArticleFactory(DjangoModelFactory):
|
|
1248
|
+
class Meta:
|
|
1249
|
+
model = Article
|
|
1250
|
+
|
|
1251
|
+
title = factory.Sequence(lambda n: f"Article {n}")
|
|
1252
|
+
slug = factory.LazyAttribute(lambda o: o.title.lower().replace(" ", "-"))
|
|
1253
|
+
body = factory.Faker("paragraph", nb_sentences=10)
|
|
1254
|
+
author = factory.SubFactory(UserFactory)
|
|
1255
|
+
category = factory.SubFactory(CategoryFactory)
|
|
1256
|
+
status = "draft"
|
|
1257
|
+
|
|
1258
|
+
class Params:
|
|
1259
|
+
published = factory.Trait(
|
|
1260
|
+
status="published",
|
|
1261
|
+
publish_date=factory.LazyFunction(timezone.now),
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
# 在测试中使用
|
|
1265
|
+
class ArticleTest(TestCase):
|
|
1266
|
+
def test_with_factory(self):
|
|
1267
|
+
article = ArticleFactory(published=True)
|
|
1268
|
+
self.assertEqual(article.status, "published")
|
|
1269
|
+
|
|
1270
|
+
def test_batch_create(self):
|
|
1271
|
+
ArticleFactory.create_batch(10, published=True)
|
|
1272
|
+
self.assertEqual(Article.published.count(), 10)
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### 覆盖率配置
|
|
1276
|
+
|
|
1277
|
+
```ini
|
|
1278
|
+
# .coveragerc 或 pyproject.toml
|
|
1279
|
+
[tool.coverage.run]
|
|
1280
|
+
source = ["apps"]
|
|
1281
|
+
omit = ["*/migrations/*", "*/tests/*", "*/admin.py"]
|
|
1282
|
+
branch = true
|
|
1283
|
+
|
|
1284
|
+
[tool.coverage.report]
|
|
1285
|
+
fail_under = 85
|
|
1286
|
+
show_missing = true
|
|
1287
|
+
exclude_lines = [
|
|
1288
|
+
"pragma: no cover",
|
|
1289
|
+
"def __repr__",
|
|
1290
|
+
"raise NotImplementedError",
|
|
1291
|
+
"if TYPE_CHECKING:",
|
|
1292
|
+
]
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
```bash
|
|
1296
|
+
pytest --cov=apps --cov-report=term-missing --cov-report=html
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
---
|
|
1300
|
+
|
|
1301
|
+
## 部署
|
|
1302
|
+
|
|
1303
|
+
### Gunicorn 配置
|
|
1304
|
+
|
|
1305
|
+
```python
|
|
1306
|
+
# gunicorn.conf.py
|
|
1307
|
+
import multiprocessing
|
|
1308
|
+
|
|
1309
|
+
bind = "0.0.0.0:8000"
|
|
1310
|
+
workers = multiprocessing.cpu_count() * 2 + 1
|
|
1311
|
+
worker_class = "gthread"
|
|
1312
|
+
threads = 2
|
|
1313
|
+
worker_tmp_dir = "/dev/shm"
|
|
1314
|
+
timeout = 30
|
|
1315
|
+
graceful_timeout = 30
|
|
1316
|
+
keepalive = 5
|
|
1317
|
+
max_requests = 1000
|
|
1318
|
+
max_requests_jitter = 50
|
|
1319
|
+
accesslog = "-"
|
|
1320
|
+
errorlog = "-"
|
|
1321
|
+
loglevel = "info"
|
|
1322
|
+
preload_app = True
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Nginx 配置
|
|
1326
|
+
|
|
1327
|
+
```nginx
|
|
1328
|
+
upstream django {
|
|
1329
|
+
server 127.0.0.1:8000;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
server {
|
|
1333
|
+
listen 80;
|
|
1334
|
+
server_name mysite.com;
|
|
1335
|
+
return 301 https://$host$request_uri;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
server {
|
|
1339
|
+
listen 443 ssl http2;
|
|
1340
|
+
server_name mysite.com;
|
|
1341
|
+
|
|
1342
|
+
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
1343
|
+
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
1344
|
+
|
|
1345
|
+
client_max_body_size 10M;
|
|
1346
|
+
|
|
1347
|
+
location /static/ {
|
|
1348
|
+
alias /app/staticfiles/;
|
|
1349
|
+
expires 30d;
|
|
1350
|
+
add_header Cache-Control "public, immutable";
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
location /media/ {
|
|
1354
|
+
alias /app/media/;
|
|
1355
|
+
expires 7d;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
location / {
|
|
1359
|
+
proxy_pass http://django;
|
|
1360
|
+
proxy_set_header Host $host;
|
|
1361
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
1362
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1363
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1364
|
+
proxy_read_timeout 30s;
|
|
1365
|
+
proxy_connect_timeout 10s;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
### Docker 部署
|
|
1371
|
+
|
|
1372
|
+
```dockerfile
|
|
1373
|
+
# Dockerfile
|
|
1374
|
+
FROM python:3.12-slim AS base
|
|
1375
|
+
|
|
1376
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
1377
|
+
PYTHONUNBUFFERED=1
|
|
1378
|
+
|
|
1379
|
+
WORKDIR /app
|
|
1380
|
+
|
|
1381
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
1382
|
+
libpq-dev gcc && \
|
|
1383
|
+
rm -rf /var/lib/apt/lists/*
|
|
1384
|
+
|
|
1385
|
+
COPY requirements/prod.txt requirements.txt
|
|
1386
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
1387
|
+
|
|
1388
|
+
COPY . .
|
|
1389
|
+
RUN python manage.py collectstatic --noinput
|
|
1390
|
+
|
|
1391
|
+
FROM base AS production
|
|
1392
|
+
RUN addgroup --system django && adduser --system --group django
|
|
1393
|
+
USER django
|
|
1394
|
+
|
|
1395
|
+
CMD ["gunicorn", "config.wsgi:application", "-c", "gunicorn.conf.py"]
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
```yaml
|
|
1399
|
+
# docker-compose.yml
|
|
1400
|
+
services:
|
|
1401
|
+
web:
|
|
1402
|
+
build: .
|
|
1403
|
+
command: gunicorn config.wsgi:application -c gunicorn.conf.py
|
|
1404
|
+
volumes:
|
|
1405
|
+
- static_volume:/app/staticfiles
|
|
1406
|
+
- media_volume:/app/media
|
|
1407
|
+
env_file: .env
|
|
1408
|
+
depends_on:
|
|
1409
|
+
db:
|
|
1410
|
+
condition: service_healthy
|
|
1411
|
+
redis:
|
|
1412
|
+
condition: service_started
|
|
1413
|
+
|
|
1414
|
+
db:
|
|
1415
|
+
image: postgres:16-alpine
|
|
1416
|
+
volumes:
|
|
1417
|
+
- postgres_data:/var/lib/postgresql/data
|
|
1418
|
+
environment:
|
|
1419
|
+
POSTGRES_DB: ${DB_NAME}
|
|
1420
|
+
POSTGRES_USER: ${DB_USER}
|
|
1421
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
1422
|
+
healthcheck:
|
|
1423
|
+
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
|
|
1424
|
+
interval: 5s
|
|
1425
|
+
timeout: 5s
|
|
1426
|
+
retries: 5
|
|
1427
|
+
|
|
1428
|
+
redis:
|
|
1429
|
+
image: redis:7-alpine
|
|
1430
|
+
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
|
|
1431
|
+
|
|
1432
|
+
celery:
|
|
1433
|
+
build: .
|
|
1434
|
+
command: celery -A config worker -l info --concurrency=4
|
|
1435
|
+
env_file: .env
|
|
1436
|
+
depends_on: [web, redis]
|
|
1437
|
+
|
|
1438
|
+
celery-beat:
|
|
1439
|
+
build: .
|
|
1440
|
+
command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
|
1441
|
+
env_file: .env
|
|
1442
|
+
depends_on: [web, redis]
|
|
1443
|
+
|
|
1444
|
+
nginx:
|
|
1445
|
+
image: nginx:alpine
|
|
1446
|
+
ports:
|
|
1447
|
+
- "80:80"
|
|
1448
|
+
- "443:443"
|
|
1449
|
+
volumes:
|
|
1450
|
+
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
|
1451
|
+
- static_volume:/app/staticfiles
|
|
1452
|
+
- media_volume:/app/media
|
|
1453
|
+
|
|
1454
|
+
volumes:
|
|
1455
|
+
postgres_data:
|
|
1456
|
+
static_volume:
|
|
1457
|
+
media_volume:
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
### Kubernetes 部署
|
|
1461
|
+
|
|
1462
|
+
```yaml
|
|
1463
|
+
# k8s/deployment.yaml
|
|
1464
|
+
apiVersion: apps/v1
|
|
1465
|
+
kind: Deployment
|
|
1466
|
+
metadata:
|
|
1467
|
+
name: django-web
|
|
1468
|
+
spec:
|
|
1469
|
+
replicas: 3
|
|
1470
|
+
selector:
|
|
1471
|
+
matchLabels:
|
|
1472
|
+
app: django-web
|
|
1473
|
+
template:
|
|
1474
|
+
metadata:
|
|
1475
|
+
labels:
|
|
1476
|
+
app: django-web
|
|
1477
|
+
spec:
|
|
1478
|
+
containers:
|
|
1479
|
+
- name: web
|
|
1480
|
+
image: myregistry/django-app:latest
|
|
1481
|
+
ports:
|
|
1482
|
+
- containerPort: 8000
|
|
1483
|
+
envFrom:
|
|
1484
|
+
- secretRef:
|
|
1485
|
+
name: django-secrets
|
|
1486
|
+
- configMapRef:
|
|
1487
|
+
name: django-config
|
|
1488
|
+
resources:
|
|
1489
|
+
requests:
|
|
1490
|
+
cpu: "250m"
|
|
1491
|
+
memory: "256Mi"
|
|
1492
|
+
limits:
|
|
1493
|
+
cpu: "1000m"
|
|
1494
|
+
memory: "512Mi"
|
|
1495
|
+
readinessProbe:
|
|
1496
|
+
httpGet:
|
|
1497
|
+
path: /health/
|
|
1498
|
+
port: 8000
|
|
1499
|
+
initialDelaySeconds: 10
|
|
1500
|
+
periodSeconds: 5
|
|
1501
|
+
livenessProbe:
|
|
1502
|
+
httpGet:
|
|
1503
|
+
path: /health/
|
|
1504
|
+
port: 8000
|
|
1505
|
+
initialDelaySeconds: 30
|
|
1506
|
+
periodSeconds: 15
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
---
|
|
1510
|
+
|
|
1511
|
+
## 性能优化
|
|
1512
|
+
|
|
1513
|
+
### 数据库查询优化
|
|
1514
|
+
|
|
1515
|
+
```python
|
|
1516
|
+
# 1. 使用 select_related 减少 ForeignKey 查询
|
|
1517
|
+
# 差: 每篇文章额外查询 author 和 category
|
|
1518
|
+
articles = Article.objects.all()
|
|
1519
|
+
for a in articles:
|
|
1520
|
+
print(a.author.email) # N+1 查询!
|
|
1521
|
+
|
|
1522
|
+
# 好: 单次 JOIN 查询
|
|
1523
|
+
articles = Article.objects.select_related("author", "category").all()
|
|
1524
|
+
|
|
1525
|
+
# 2. 使用 prefetch_related 处理 ManyToMany
|
|
1526
|
+
articles = Article.objects.prefetch_related("tags").all()
|
|
1527
|
+
|
|
1528
|
+
# 3. 只查需要的字段
|
|
1529
|
+
Article.objects.values_list("id", "title", flat=False)
|
|
1530
|
+
Article.objects.only("id", "title", "status")
|
|
1531
|
+
Article.objects.defer("body") # 延迟加载大字段
|
|
1532
|
+
|
|
1533
|
+
# 4. 使用 iterator() 处理大结果集
|
|
1534
|
+
for article in Article.objects.all().iterator(chunk_size=2000):
|
|
1535
|
+
process(article)
|
|
1536
|
+
|
|
1537
|
+
# 5. 批量操作
|
|
1538
|
+
Article.objects.bulk_create([
|
|
1539
|
+
Article(title=f"Article {i}", body="...", author=user)
|
|
1540
|
+
for i in range(1000)
|
|
1541
|
+
], batch_size=500)
|
|
1542
|
+
|
|
1543
|
+
Article.objects.filter(status="draft").update(status="archived")
|
|
1544
|
+
|
|
1545
|
+
Article.objects.bulk_update(articles, ["status", "updated_at"], batch_size=500)
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
### 索引策略
|
|
1549
|
+
|
|
1550
|
+
```python
|
|
1551
|
+
class Article(models.Model):
|
|
1552
|
+
class Meta:
|
|
1553
|
+
indexes = [
|
|
1554
|
+
# 单字段索引
|
|
1555
|
+
models.Index(fields=["status"]),
|
|
1556
|
+
# 复合索引 —— 查询条件的列顺序要与索引一致
|
|
1557
|
+
models.Index(fields=["status", "-publish_date"]),
|
|
1558
|
+
# 部分索引(PostgreSQL)
|
|
1559
|
+
models.Index(
|
|
1560
|
+
fields=["publish_date"],
|
|
1561
|
+
condition=Q(status="published"),
|
|
1562
|
+
name="idx_published_date",
|
|
1563
|
+
),
|
|
1564
|
+
# 覆盖索引(PostgreSQL,Django 5.0+)
|
|
1565
|
+
models.Index(
|
|
1566
|
+
fields=["status"],
|
|
1567
|
+
include=["title", "publish_date"],
|
|
1568
|
+
name="idx_status_covering",
|
|
1569
|
+
),
|
|
1570
|
+
]
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
### 查询调试
|
|
1574
|
+
|
|
1575
|
+
```python
|
|
1576
|
+
# 在开发环境中追踪查询
|
|
1577
|
+
from django.db import connection, reset_queries
|
|
1578
|
+
|
|
1579
|
+
reset_queries()
|
|
1580
|
+
# ... 执行查询 ...
|
|
1581
|
+
print(f"Total queries: {len(connection.queries)}")
|
|
1582
|
+
for q in connection.queries:
|
|
1583
|
+
print(f"[{q['time']}s] {q['sql'][:200]}")
|
|
1584
|
+
|
|
1585
|
+
# 使用 django-debug-toolbar
|
|
1586
|
+
INSTALLED_APPS += ["debug_toolbar"]
|
|
1587
|
+
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
|
|
1588
|
+
INTERNAL_IPS = ["127.0.0.1"]
|
|
1589
|
+
|
|
1590
|
+
# QuerySet.explain() 查看执行计划
|
|
1591
|
+
qs = Article.objects.filter(status="published").order_by("-publish_date")
|
|
1592
|
+
print(qs.explain(analyze=True))
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
## 常见陷阱
|
|
1598
|
+
|
|
1599
|
+
### N+1 查询
|
|
1600
|
+
|
|
1601
|
+
```python
|
|
1602
|
+
# 陷阱: 模板或序列化器中触发隐式查询
|
|
1603
|
+
# 检测方法: django-debug-toolbar / django-silk / nplusone
|
|
1604
|
+
|
|
1605
|
+
# 解决方案: 在 QuerySet 层提前 JOIN 或预取
|
|
1606
|
+
# View 层负责定义 QuerySet,Serializer 不应触发额外查询
|
|
1607
|
+
|
|
1608
|
+
class ArticleViewSet(viewsets.ModelViewSet):
|
|
1609
|
+
def get_queryset(self):
|
|
1610
|
+
return (
|
|
1611
|
+
Article.objects
|
|
1612
|
+
.select_related("author", "category")
|
|
1613
|
+
.prefetch_related("tags", "comments__author")
|
|
1614
|
+
.annotate(comment_count=Count("comments"))
|
|
1615
|
+
)
|
|
1616
|
+
```
|
|
1617
|
+
|
|
1618
|
+
### 信号滥用
|
|
1619
|
+
|
|
1620
|
+
```python
|
|
1621
|
+
# 陷阱: 把业务逻辑放在信号中
|
|
1622
|
+
# - 隐式执行,难以调试和追踪
|
|
1623
|
+
# - 多个信号之间可能存在顺序依赖
|
|
1624
|
+
# - 信号异常可能被吞掉
|
|
1625
|
+
|
|
1626
|
+
# 解决方案: 使用 Service 层替代
|
|
1627
|
+
class ArticleService:
|
|
1628
|
+
@staticmethod
|
|
1629
|
+
def publish(article):
|
|
1630
|
+
article.status = "published"
|
|
1631
|
+
article.publish_date = timezone.now()
|
|
1632
|
+
article.save(update_fields=["status", "publish_date"])
|
|
1633
|
+
# 明确的后续操作
|
|
1634
|
+
send_notification_email.delay(article.author_id, "article_published.html", {...})
|
|
1635
|
+
cache.delete(f"article:{article.id}")
|
|
1636
|
+
return article
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
### Fat Models 反模式
|
|
1640
|
+
|
|
1641
|
+
```python
|
|
1642
|
+
# 陷阱: Model 中包含大量业务逻辑、外部调用、邮件发送
|
|
1643
|
+
class Article(models.Model):
|
|
1644
|
+
def publish(self):
|
|
1645
|
+
self.status = "published"
|
|
1646
|
+
self.save()
|
|
1647
|
+
send_mail(...) # 模型不应直接发邮件
|
|
1648
|
+
requests.post(...) # 模型不应调用外部服务
|
|
1649
|
+
cache.delete(...) # 模型不应管理缓存
|
|
1650
|
+
|
|
1651
|
+
# 解决方案: 将业务逻辑抽到 Service 层
|
|
1652
|
+
# Model 只负责: 字段定义、Meta、__str__、简单计算属性、自定义 Manager
|
|
1653
|
+
# Service 层负责: 业务流程、外部调用、缓存、通知
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
### 其他常见陷阱
|
|
1657
|
+
|
|
1658
|
+
```python
|
|
1659
|
+
# 1. 在循环中 save() —— 应使用 bulk_update
|
|
1660
|
+
for article in articles:
|
|
1661
|
+
article.view_count += 1
|
|
1662
|
+
article.save() # 每次都是一条 UPDATE!
|
|
1663
|
+
|
|
1664
|
+
# 修正
|
|
1665
|
+
Article.objects.filter(id__in=ids).update(view_count=F("view_count") + 1)
|
|
1666
|
+
|
|
1667
|
+
# 2. 未正确使用事务
|
|
1668
|
+
from django.db import transaction
|
|
1669
|
+
|
|
1670
|
+
# 差: 部分成功部分失败
|
|
1671
|
+
def transfer_funds(from_account, to_account, amount):
|
|
1672
|
+
from_account.balance -= amount
|
|
1673
|
+
from_account.save()
|
|
1674
|
+
# 如果这里崩溃,钱就消失了!
|
|
1675
|
+
to_account.balance += amount
|
|
1676
|
+
to_account.save()
|
|
1677
|
+
|
|
1678
|
+
# 好: 原子事务
|
|
1679
|
+
@transaction.atomic
|
|
1680
|
+
def transfer_funds(from_account, to_account, amount):
|
|
1681
|
+
from_account.balance = F("balance") - amount
|
|
1682
|
+
from_account.save(update_fields=["balance"])
|
|
1683
|
+
to_account.balance = F("balance") + amount
|
|
1684
|
+
to_account.save(update_fields=["balance"])
|
|
1685
|
+
|
|
1686
|
+
# 3. settings.py 中硬编码秘密信息
|
|
1687
|
+
SECRET_KEY = "hardcoded-secret" # 永远不要这样做!
|
|
1688
|
+
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # 从环境变量读取
|
|
1689
|
+
|
|
1690
|
+
# 4. 迁移中使用当前模型代码
|
|
1691
|
+
# 差: 迁移运行时模型可能已变
|
|
1692
|
+
from apps.users.models import User
|
|
1693
|
+
|
|
1694
|
+
# 好: 使用历史模型
|
|
1695
|
+
def forward(apps, schema_editor):
|
|
1696
|
+
User = apps.get_model("users", "User")
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
---
|
|
1700
|
+
|
|
1701
|
+
## Agent Checklist
|
|
1702
|
+
|
|
1703
|
+
以下是 UmaDev Agent 在 Django 项目中执行审查和交付时必须验证的检查清单:
|
|
1704
|
+
|
|
1705
|
+
### 项目结构
|
|
1706
|
+
- [ ] 使用 settings 拆分(base/dev/staging/prod)
|
|
1707
|
+
- [ ] `AUTH_USER_MODEL` 在第一次迁移前已定义
|
|
1708
|
+
- [ ] 每个 app 有独立的 `tests/` 目录和 `factories.py`
|
|
1709
|
+
- [ ] requirements 按环境拆分(base/dev/prod)
|
|
1710
|
+
|
|
1711
|
+
### ORM 与数据库
|
|
1712
|
+
- [ ] 所有 ForeignKey 查询使用 `select_related`
|
|
1713
|
+
- [ ] 所有 ManyToMany / 反向查询使用 `prefetch_related`
|
|
1714
|
+
- [ ] 列表接口使用 `only()` / `defer()` 排除大字段
|
|
1715
|
+
- [ ] 批量操作使用 `bulk_create` / `bulk_update` 而非循环 save
|
|
1716
|
+
- [ ] 高频查询字段有 `db_index=True` 或 Meta.indexes
|
|
1717
|
+
- [ ] 关键写操作包裹在 `transaction.atomic` 中
|
|
1718
|
+
- [ ] 迁移中使用 `apps.get_model()` 而非直接 import
|
|
1719
|
+
|
|
1720
|
+
### 认证与权限
|
|
1721
|
+
- [ ] 自定义 User 模型继承 `AbstractUser` 或 `AbstractBaseUser`
|
|
1722
|
+
- [ ] JWT 配置了 token 过期时间和刷新轮换
|
|
1723
|
+
- [ ] API ViewSet 显式声明 `permission_classes`
|
|
1724
|
+
- [ ] 对象级权限已实现(如 IsAuthorOrReadOnly)
|
|
1725
|
+
|
|
1726
|
+
### REST API
|
|
1727
|
+
- [ ] 列表和详情使用不同 Serializer(轻量 vs 完整)
|
|
1728
|
+
- [ ] 分页已配置(推荐 CursorPagination 用于大数据集)
|
|
1729
|
+
- [ ] 限流已配置(区分匿名和认证用户)
|
|
1730
|
+
- [ ] 输入验证在 Serializer 层完成(validate_field / validate)
|
|
1731
|
+
|
|
1732
|
+
### 缓存
|
|
1733
|
+
- [ ] Redis 连接池大小和超时已配置
|
|
1734
|
+
- [ ] 缓存 key 有命名规范和前缀
|
|
1735
|
+
- [ ] 数据变更时有缓存失效策略
|
|
1736
|
+
- [ ] 不缓存包含用户敏感信息的响应
|
|
1737
|
+
|
|
1738
|
+
### 异步任务
|
|
1739
|
+
- [ ] Celery 任务配置了 `max_retries` 和 `time_limit`
|
|
1740
|
+
- [ ] 使用 `retry_backoff=True` 避免重试风暴
|
|
1741
|
+
- [ ] 任务参数可 JSON 序列化(不传 Model 实例,传 ID)
|
|
1742
|
+
- [ ] 定时任务通过 Beat 管理,不使用 cron 直调
|
|
1743
|
+
|
|
1744
|
+
### 安全
|
|
1745
|
+
- [ ] 生产环境 `DEBUG=False`
|
|
1746
|
+
- [ ] `SECRET_KEY` 从环境变量读取
|
|
1747
|
+
- [ ] HSTS/SSL/Secure Cookie 已启用
|
|
1748
|
+
- [ ] CSRF 保护正确配置(Session 认证开启,纯 JWT 可排除)
|
|
1749
|
+
- [ ] 用户输入 HTML 经过 `bleach` 清理
|
|
1750
|
+
- [ ] Raw SQL 使用参数化查询
|
|
1751
|
+
- [ ] CSP 头已配置
|
|
1752
|
+
|
|
1753
|
+
### 测试
|
|
1754
|
+
- [ ] 单元测试覆盖率 >= 85%
|
|
1755
|
+
- [ ] 使用 Factory Boy 而非手动创建测试数据
|
|
1756
|
+
- [ ] API 测试覆盖认证、权限、分页、错误场景
|
|
1757
|
+
- [ ] 使用 `setUpTestData` 替代 `setUp` 提升速度
|
|
1758
|
+
|
|
1759
|
+
### 部署
|
|
1760
|
+
- [ ] Gunicorn workers 数量 = CPU * 2 + 1
|
|
1761
|
+
- [ ] Nginx 配置静态文件直接服务和缓存头
|
|
1762
|
+
- [ ] Docker 镜像使用非 root 用户运行
|
|
1763
|
+
- [ ] 健康检查端点 `/health/` 已实现
|
|
1764
|
+
- [ ] 数据库连接配置 `CONN_MAX_AGE` 和超时
|
|
1765
|
+
- [ ] Celery worker 配置了 `max_tasks_per_child` 防止内存泄漏
|