@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,2167 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: nestjs-complete
|
|
3
|
+
title: NestJS 完整指南
|
|
4
|
+
domain: backend
|
|
5
|
+
category: 01-standards
|
|
6
|
+
difficulty: intermediate
|
|
7
|
+
tags: [backend, complete, nestjs, websocket, 依赖注入系统, 微服务, 快速开始, 数据库集成]
|
|
8
|
+
quality_score: 70
|
|
9
|
+
last_updated: 2026-06-15
|
|
10
|
+
---
|
|
11
|
+
# NestJS 完整指南
|
|
12
|
+
|
|
13
|
+
## 概述
|
|
14
|
+
|
|
15
|
+
NestJS 是一个用于构建高效、可扩展 Node.js 服务端应用的渐进式框架。它完全使用 TypeScript 构建(同时兼容纯 JavaScript),融合了面向对象编程 (OOP)、函数式编程 (FP) 和函数式响应式编程 (FRP) 的理念。底层默认使用 Express,但也支持切换到 Fastify 以获得更高性能。
|
|
16
|
+
|
|
17
|
+
### NestJS vs Express vs Fastify
|
|
18
|
+
|
|
19
|
+
| 特性 | NestJS | Express | Fastify |
|
|
20
|
+
|------|--------|---------|---------|
|
|
21
|
+
| 架构模式 | 模块化 + 依赖注入 | 无约定,极简 | 插件体系 |
|
|
22
|
+
| TypeScript 支持 | 原生一等公民 | 需手动配置 | 内置支持 |
|
|
23
|
+
| 性能(req/s 基准) | 与底层适配器一致 | ~15k | ~30k |
|
|
24
|
+
| 学习曲线 | 中高(Angular 风格) | 低 | 低-中 |
|
|
25
|
+
| 依赖注入 | 内置 IoC 容器 | 无 | 无 |
|
|
26
|
+
| 微服务支持 | 内置多种 Transport | 无(需第三方) | 无(需第三方) |
|
|
27
|
+
| WebSocket | 内置 Gateway | 需 socket.io | 需插件 |
|
|
28
|
+
| GraphQL | 官方模块 | 需 Apollo 手动集成 | 需 Mercurius |
|
|
29
|
+
| 测试工具 | Testing 模块 + 自动 Mock | 需自行搭建 | 需自行搭建 |
|
|
30
|
+
| 企业级就绪度 | 高 | 中 | 中 |
|
|
31
|
+
|
|
32
|
+
### 何时选择 NestJS
|
|
33
|
+
|
|
34
|
+
✅ **大型团队协作**: 强约定 + 模块化架构减少代码风格分歧
|
|
35
|
+
✅ **微服务体系**: 内置 Transport 层,支持 Redis/Kafka/gRPC/NATS/MQTT
|
|
36
|
+
✅ **企业级后端**: 完善的认证/授权/缓存/队列/日志/健康检查生态
|
|
37
|
+
✅ **全栈 TypeScript**: 前后端共享类型定义,配合 Monorepo 高效开发
|
|
38
|
+
✅ **需要长期维护的系统**: 模块边界清晰,重构成本低
|
|
39
|
+
|
|
40
|
+
❌ **简单 API / 原型验证**: 过度工程化,Express/Fastify 更轻便
|
|
41
|
+
❌ **极致性能场景**: 框架层带来一定开销,考虑 Fastify 或 Go
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
### 安装
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 使用 CLI 创建项目(推荐)
|
|
51
|
+
npm i -g @nestjs/cli
|
|
52
|
+
nest new my-project
|
|
53
|
+
|
|
54
|
+
# 手动安装核心包
|
|
55
|
+
npm i @nestjs/core @nestjs/common @nestjs/platform-express reflect-metadata rxjs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Hello World
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// main.ts
|
|
62
|
+
import { NestFactory } from '@nestjs/core';
|
|
63
|
+
import { AppModule } from './app.module';
|
|
64
|
+
|
|
65
|
+
async function bootstrap() {
|
|
66
|
+
const app = await NestFactory.create(AppModule);
|
|
67
|
+
await app.listen(3000);
|
|
68
|
+
}
|
|
69
|
+
bootstrap();
|
|
70
|
+
|
|
71
|
+
// app.module.ts
|
|
72
|
+
import { Module } from '@nestjs/common';
|
|
73
|
+
import { AppController } from './app.controller';
|
|
74
|
+
import { AppService } from './app.service';
|
|
75
|
+
|
|
76
|
+
@Module({
|
|
77
|
+
imports: [],
|
|
78
|
+
controllers: [AppController],
|
|
79
|
+
providers: [AppService],
|
|
80
|
+
})
|
|
81
|
+
export class AppModule {}
|
|
82
|
+
|
|
83
|
+
// app.controller.ts
|
|
84
|
+
import { Controller, Get } from '@nestjs/common';
|
|
85
|
+
import { AppService } from './app.service';
|
|
86
|
+
|
|
87
|
+
@Controller()
|
|
88
|
+
export class AppController {
|
|
89
|
+
constructor(private readonly appService: AppService) {}
|
|
90
|
+
|
|
91
|
+
@Get()
|
|
92
|
+
getHello(): string {
|
|
93
|
+
return this.appService.getHello();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// app.service.ts
|
|
98
|
+
import { Injectable } from '@nestjs/common';
|
|
99
|
+
|
|
100
|
+
@Injectable()
|
|
101
|
+
export class AppService {
|
|
102
|
+
getHello(): string {
|
|
103
|
+
return 'Hello World!';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### CLI 常用命令
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
nest generate module users # 生成模块
|
|
112
|
+
nest generate controller users # 生成控制器
|
|
113
|
+
nest generate service users # 生成服务
|
|
114
|
+
nest generate resource users # 一键生成完整 CRUD 资源(模块+控制器+服务+DTO+实体)
|
|
115
|
+
nest build # 编译项目
|
|
116
|
+
nest start --watch # 开发模式热重载
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 核心概念
|
|
122
|
+
|
|
123
|
+
### 1. 模块 (Modules)
|
|
124
|
+
|
|
125
|
+
模块是 NestJS 应用的基本组织单元。每个应用至少有一个根模块 (AppModule)。
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { Module, Global } from '@nestjs/common';
|
|
129
|
+
import { UsersController } from './users.controller';
|
|
130
|
+
import { UsersService } from './users.service';
|
|
131
|
+
import { AuthModule } from '../auth/auth.module';
|
|
132
|
+
|
|
133
|
+
@Module({
|
|
134
|
+
imports: [AuthModule], // 导入其他模块
|
|
135
|
+
controllers: [UsersController], // 注册控制器
|
|
136
|
+
providers: [UsersService], // 注册提供者
|
|
137
|
+
exports: [UsersService], // 导出供其他模块使用
|
|
138
|
+
})
|
|
139
|
+
export class UsersModule {}
|
|
140
|
+
|
|
141
|
+
// 全局模块 — 导入一次,全局可用
|
|
142
|
+
@Global()
|
|
143
|
+
@Module({
|
|
144
|
+
providers: [ConfigService],
|
|
145
|
+
exports: [ConfigService],
|
|
146
|
+
})
|
|
147
|
+
export class ConfigModule {}
|
|
148
|
+
|
|
149
|
+
// 动态模块 — 根据参数动态配置
|
|
150
|
+
@Module({})
|
|
151
|
+
export class DatabaseModule {
|
|
152
|
+
static forRoot(options: DatabaseOptions): DynamicModule {
|
|
153
|
+
return {
|
|
154
|
+
module: DatabaseModule,
|
|
155
|
+
providers: [
|
|
156
|
+
{
|
|
157
|
+
provide: 'DATABASE_OPTIONS',
|
|
158
|
+
useValue: options,
|
|
159
|
+
},
|
|
160
|
+
DatabaseService,
|
|
161
|
+
],
|
|
162
|
+
exports: [DatabaseService],
|
|
163
|
+
global: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
static forFeature(entities: Type[]): DynamicModule {
|
|
168
|
+
const providers = entities.map((entity) => ({
|
|
169
|
+
provide: getRepositoryToken(entity),
|
|
170
|
+
useFactory: (ds: DataSource) => ds.getRepository(entity),
|
|
171
|
+
inject: [DataSource],
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
module: DatabaseModule,
|
|
175
|
+
providers,
|
|
176
|
+
exports: providers,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. 控制器 (Controllers)
|
|
183
|
+
|
|
184
|
+
控制器负责处理传入请求并返回响应。
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import {
|
|
188
|
+
Controller, Get, Post, Put, Delete, Patch,
|
|
189
|
+
Param, Query, Body, Headers, Ip,
|
|
190
|
+
HttpCode, HttpStatus, Redirect, Header,
|
|
191
|
+
Res, Req, ParseIntPipe, ParseUUIDPipe,
|
|
192
|
+
UseGuards, UseInterceptors, UsePipes,
|
|
193
|
+
} from '@nestjs/common';
|
|
194
|
+
import { Request, Response } from 'express';
|
|
195
|
+
|
|
196
|
+
@Controller('users') // 路由前缀 /users
|
|
197
|
+
export class UsersController {
|
|
198
|
+
constructor(private readonly usersService: UsersService) {}
|
|
199
|
+
|
|
200
|
+
@Get()
|
|
201
|
+
findAll(@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number) {
|
|
202
|
+
return this.usersService.findAll(page);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Get(':id')
|
|
206
|
+
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
|
207
|
+
return this.usersService.findOne(id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@Post()
|
|
211
|
+
@HttpCode(HttpStatus.CREATED)
|
|
212
|
+
create(@Body() createUserDto: CreateUserDto) {
|
|
213
|
+
return this.usersService.create(createUserDto);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Put(':id')
|
|
217
|
+
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
|
218
|
+
return this.usersService.update(id, updateUserDto);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Delete(':id')
|
|
222
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
223
|
+
remove(@Param('id') id: string) {
|
|
224
|
+
return this.usersService.remove(id);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 子路由分组
|
|
228
|
+
@Get(':id/orders')
|
|
229
|
+
findUserOrders(@Param('id') id: string) {
|
|
230
|
+
return this.usersService.findOrders(id);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 自定义响应(绕过序列化)
|
|
234
|
+
@Get(':id/avatar')
|
|
235
|
+
async getAvatar(@Param('id') id: string, @Res() res: Response) {
|
|
236
|
+
const stream = await this.usersService.getAvatar(id);
|
|
237
|
+
stream.pipe(res);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 版本控制
|
|
242
|
+
@Controller({ path: 'users', version: '2' })
|
|
243
|
+
export class UsersV2Controller {
|
|
244
|
+
// /v2/users
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 3. 提供者 (Providers)
|
|
249
|
+
|
|
250
|
+
提供者是 NestJS 依赖注入的核心。任何被 `@Injectable()` 修饰的类都可作为提供者。
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
254
|
+
|
|
255
|
+
@Injectable()
|
|
256
|
+
export class UsersService {
|
|
257
|
+
constructor(
|
|
258
|
+
@Inject('USER_REPOSITORY') private userRepo: Repository<User>,
|
|
259
|
+
private readonly configService: ConfigService,
|
|
260
|
+
) {}
|
|
261
|
+
|
|
262
|
+
async findAll(page: number): Promise<PaginatedResult<User>> {
|
|
263
|
+
const take = this.configService.get<number>('PAGE_SIZE', 20);
|
|
264
|
+
const skip = (page - 1) * take;
|
|
265
|
+
const [data, total] = await this.userRepo.findAndCount({ take, skip });
|
|
266
|
+
return { data, total, page, pageSize: take };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async findOne(id: string): Promise<User> {
|
|
270
|
+
const user = await this.userRepo.findOne({ where: { id } });
|
|
271
|
+
if (!user) {
|
|
272
|
+
throw new NotFoundException(`User #${id} not found`);
|
|
273
|
+
}
|
|
274
|
+
return user;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async create(dto: CreateUserDto): Promise<User> {
|
|
278
|
+
const user = this.userRepo.create(dto);
|
|
279
|
+
return this.userRepo.save(user);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 4. 中间件 (Middleware)
|
|
285
|
+
|
|
286
|
+
在路由处理器之前执行,可访问请求/响应对象。
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
290
|
+
import { Request, Response, NextFunction } from 'express';
|
|
291
|
+
|
|
292
|
+
@Injectable()
|
|
293
|
+
export class LoggerMiddleware implements NestMiddleware {
|
|
294
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
295
|
+
const start = Date.now();
|
|
296
|
+
res.on('finish', () => {
|
|
297
|
+
const duration = Date.now() - start;
|
|
298
|
+
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
|
|
299
|
+
});
|
|
300
|
+
next();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 在模块中注册
|
|
305
|
+
@Module({})
|
|
306
|
+
export class AppModule implements NestModule {
|
|
307
|
+
configure(consumer: MiddlewareConsumer) {
|
|
308
|
+
consumer
|
|
309
|
+
.apply(LoggerMiddleware, CorsMiddleware)
|
|
310
|
+
.exclude({ path: 'health', method: RequestMethod.GET })
|
|
311
|
+
.forRoutes('*');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 函数式中间件(简单场景)
|
|
316
|
+
export function helmet(req: Request, res: Response, next: NextFunction) {
|
|
317
|
+
// 安全头设置
|
|
318
|
+
next();
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### 5. 管道 (Pipes)
|
|
323
|
+
|
|
324
|
+
用于数据转换和验证。在路由处理器执行之前处理参数。
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import {
|
|
328
|
+
PipeTransform, Injectable, ArgumentMetadata,
|
|
329
|
+
BadRequestException,
|
|
330
|
+
} from '@nestjs/common';
|
|
331
|
+
import { validate } from 'class-validator';
|
|
332
|
+
import { plainToInstance } from 'class-transformer';
|
|
333
|
+
|
|
334
|
+
// 全局验证管道(推荐在 main.ts 设置)
|
|
335
|
+
async function bootstrap() {
|
|
336
|
+
const app = await NestFactory.create(AppModule);
|
|
337
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
338
|
+
whitelist: true, // 剥离非 DTO 定义的属性
|
|
339
|
+
forbidNonWhitelisted: true, // 存在非白名单属性时抛出异常
|
|
340
|
+
transform: true, // 自动类型转换
|
|
341
|
+
transformOptions: {
|
|
342
|
+
enableImplicitConversion: true,
|
|
343
|
+
},
|
|
344
|
+
}));
|
|
345
|
+
await app.listen(3000);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 自定义管道
|
|
349
|
+
@Injectable()
|
|
350
|
+
export class ParseDatePipe implements PipeTransform<string, Date> {
|
|
351
|
+
transform(value: string, metadata: ArgumentMetadata): Date {
|
|
352
|
+
const date = new Date(value);
|
|
353
|
+
if (isNaN(date.getTime())) {
|
|
354
|
+
throw new BadRequestException(`"${value}" is not a valid date`);
|
|
355
|
+
}
|
|
356
|
+
return date;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// DTO 验证示例
|
|
361
|
+
import { IsString, IsEmail, MinLength, IsOptional, IsEnum } from 'class-validator';
|
|
362
|
+
|
|
363
|
+
export class CreateUserDto {
|
|
364
|
+
@IsString()
|
|
365
|
+
@MinLength(2)
|
|
366
|
+
name: string;
|
|
367
|
+
|
|
368
|
+
@IsEmail()
|
|
369
|
+
email: string;
|
|
370
|
+
|
|
371
|
+
@IsString()
|
|
372
|
+
@MinLength(8)
|
|
373
|
+
password: string;
|
|
374
|
+
|
|
375
|
+
@IsOptional()
|
|
376
|
+
@IsEnum(UserRole)
|
|
377
|
+
role?: UserRole;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 6. 守卫 (Guards)
|
|
382
|
+
|
|
383
|
+
决定请求是否被路由处理器处理。用于认证和授权。
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import {
|
|
387
|
+
Injectable, CanActivate, ExecutionContext,
|
|
388
|
+
SetMetadata, UseGuards, applyDecorators,
|
|
389
|
+
} from '@nestjs/common';
|
|
390
|
+
import { Reflector } from '@nestjs/core';
|
|
391
|
+
|
|
392
|
+
// 角色守卫
|
|
393
|
+
@Injectable()
|
|
394
|
+
export class RolesGuard implements CanActivate {
|
|
395
|
+
constructor(private reflector: Reflector) {}
|
|
396
|
+
|
|
397
|
+
canActivate(context: ExecutionContext): boolean {
|
|
398
|
+
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
|
|
399
|
+
context.getHandler(),
|
|
400
|
+
context.getClass(),
|
|
401
|
+
]);
|
|
402
|
+
if (!requiredRoles) return true;
|
|
403
|
+
|
|
404
|
+
const { user } = context.switchToHttp().getRequest();
|
|
405
|
+
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 角色装饰器
|
|
410
|
+
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
|
|
411
|
+
|
|
412
|
+
// 组合装饰器(推荐模式)
|
|
413
|
+
export function Auth(...roles: Role[]) {
|
|
414
|
+
return applyDecorators(
|
|
415
|
+
Roles(...roles),
|
|
416
|
+
UseGuards(JwtAuthGuard, RolesGuard),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 使用
|
|
421
|
+
@Controller('admin')
|
|
422
|
+
export class AdminController {
|
|
423
|
+
@Get('dashboard')
|
|
424
|
+
@Auth(Role.ADMIN)
|
|
425
|
+
getDashboard() {
|
|
426
|
+
return { message: 'Admin dashboard' };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 7. 拦截器 (Interceptors)
|
|
432
|
+
|
|
433
|
+
在路由处理器之前/之后执行额外逻辑。可用于日志、缓存、响应映射、超时等。
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import {
|
|
437
|
+
Injectable, NestInterceptor, ExecutionContext, CallHandler,
|
|
438
|
+
} from '@nestjs/common';
|
|
439
|
+
import { Observable, map, tap, timeout, catchError } from 'rxjs';
|
|
440
|
+
|
|
441
|
+
// 响应包装拦截器
|
|
442
|
+
@Injectable()
|
|
443
|
+
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
|
|
444
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
|
|
445
|
+
return next.handle().pipe(
|
|
446
|
+
map((data) => ({
|
|
447
|
+
statusCode: context.switchToHttp().getResponse().statusCode,
|
|
448
|
+
data,
|
|
449
|
+
timestamp: new Date().toISOString(),
|
|
450
|
+
})),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 日志拦截器
|
|
456
|
+
@Injectable()
|
|
457
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
458
|
+
private readonly logger = new Logger(LoggingInterceptor.name);
|
|
459
|
+
|
|
460
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
461
|
+
const req = context.switchToHttp().getRequest();
|
|
462
|
+
const { method, url } = req;
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
|
|
465
|
+
return next.handle().pipe(
|
|
466
|
+
tap(() => this.logger.log(`${method} ${url} - ${Date.now() - now}ms`)),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 超时拦截器
|
|
472
|
+
@Injectable()
|
|
473
|
+
export class TimeoutInterceptor implements NestInterceptor {
|
|
474
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
475
|
+
return next.handle().pipe(
|
|
476
|
+
timeout(5000),
|
|
477
|
+
catchError((err) => {
|
|
478
|
+
if (err.name === 'TimeoutError') {
|
|
479
|
+
throw new RequestTimeoutException();
|
|
480
|
+
}
|
|
481
|
+
throw err;
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### 8. 异常过滤器 (Exception Filters)
|
|
489
|
+
|
|
490
|
+
捕获并处理未处理的异常,统一错误响应格式。
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import {
|
|
494
|
+
ExceptionFilter, Catch, ArgumentsHost,
|
|
495
|
+
HttpException, HttpStatus, Logger,
|
|
496
|
+
} from '@nestjs/common';
|
|
497
|
+
import { Request, Response } from 'express';
|
|
498
|
+
|
|
499
|
+
@Catch()
|
|
500
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
501
|
+
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
502
|
+
|
|
503
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
504
|
+
const ctx = host.switchToHttp();
|
|
505
|
+
const response = ctx.getResponse<Response>();
|
|
506
|
+
const request = ctx.getRequest<Request>();
|
|
507
|
+
|
|
508
|
+
const status =
|
|
509
|
+
exception instanceof HttpException
|
|
510
|
+
? exception.getStatus()
|
|
511
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
512
|
+
|
|
513
|
+
const message =
|
|
514
|
+
exception instanceof HttpException
|
|
515
|
+
? exception.getResponse()
|
|
516
|
+
: 'Internal server error';
|
|
517
|
+
|
|
518
|
+
const errorResponse = {
|
|
519
|
+
statusCode: status,
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
path: request.url,
|
|
522
|
+
method: request.method,
|
|
523
|
+
message: typeof message === 'string' ? message : (message as any).message,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
this.logger.error(
|
|
527
|
+
`${request.method} ${request.url} ${status}`,
|
|
528
|
+
exception instanceof Error ? exception.stack : '',
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
response.status(status).json(errorResponse);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 自定义业务异常
|
|
536
|
+
export class BusinessException extends HttpException {
|
|
537
|
+
constructor(code: string, message: string, status = HttpStatus.BAD_REQUEST) {
|
|
538
|
+
super({ code, message }, status);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## 依赖注入系统
|
|
546
|
+
|
|
547
|
+
### 提供者作用域
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import { Injectable, Scope } from '@nestjs/common';
|
|
551
|
+
|
|
552
|
+
// 默认: 单例(推荐,整个应用生命周期只创建一个实例)
|
|
553
|
+
@Injectable()
|
|
554
|
+
export class SingletonService {}
|
|
555
|
+
|
|
556
|
+
// 请求作用域(每个请求创建新实例,注意性能影响)
|
|
557
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
558
|
+
export class RequestScopedService {
|
|
559
|
+
constructor(@Inject(REQUEST) private request: Request) {}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 瞬态作用域(每次注入创建新实例)
|
|
563
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
564
|
+
export class TransientService {}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
> **性能警告**: 请求作用域会导致依赖链上的所有提供者都变为请求作用域,显著增加实例化开销。除非确实需要访问请求对象,否则使用默认单例作用域。
|
|
568
|
+
|
|
569
|
+
### 自定义提供者
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
@Module({
|
|
573
|
+
providers: [
|
|
574
|
+
// useClass — 标准类提供者
|
|
575
|
+
{ provide: UsersService, useClass: UsersService },
|
|
576
|
+
|
|
577
|
+
// useValue — 值提供者(常量、配置、Mock)
|
|
578
|
+
{ provide: 'API_KEY', useValue: process.env.API_KEY },
|
|
579
|
+
{ provide: 'CONFIG', useValue: { retries: 3, timeout: 5000 } },
|
|
580
|
+
|
|
581
|
+
// useFactory — 工厂提供者(异步初始化、条件逻辑)
|
|
582
|
+
{
|
|
583
|
+
provide: 'ASYNC_CONNECTION',
|
|
584
|
+
useFactory: async (configService: ConfigService) => {
|
|
585
|
+
const conn = await createConnection(configService.get('DATABASE_URL'));
|
|
586
|
+
return conn;
|
|
587
|
+
},
|
|
588
|
+
inject: [ConfigService],
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// useExisting — 别名提供者
|
|
592
|
+
{ provide: 'AliasedService', useExisting: UsersService },
|
|
593
|
+
],
|
|
594
|
+
})
|
|
595
|
+
export class AppModule {}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 循环依赖解决
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// 方法 1: forwardRef(推荐)
|
|
602
|
+
@Injectable()
|
|
603
|
+
export class CatsService {
|
|
604
|
+
constructor(
|
|
605
|
+
@Inject(forwardRef(() => DogsService))
|
|
606
|
+
private dogsService: DogsService,
|
|
607
|
+
) {}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
@Injectable()
|
|
611
|
+
export class DogsService {
|
|
612
|
+
constructor(
|
|
613
|
+
@Inject(forwardRef(() => CatsService))
|
|
614
|
+
private catsService: CatsService,
|
|
615
|
+
) {}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 模块级别同理
|
|
619
|
+
@Module({
|
|
620
|
+
imports: [forwardRef(() => DogsModule)],
|
|
621
|
+
})
|
|
622
|
+
export class CatsModule {}
|
|
623
|
+
|
|
624
|
+
// 方法 2: ModuleRef(运行时解析,避免循环)
|
|
625
|
+
@Injectable()
|
|
626
|
+
export class CatsService implements OnModuleInit {
|
|
627
|
+
private dogsService: DogsService;
|
|
628
|
+
|
|
629
|
+
constructor(private moduleRef: ModuleRef) {}
|
|
630
|
+
|
|
631
|
+
onModuleInit() {
|
|
632
|
+
this.dogsService = this.moduleRef.get(DogsService, { strict: false });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
> **最佳实践**: 循环依赖通常是架构设计的坏味道。优先考虑提取公共逻辑到第三个服务,或使用事件驱动模式解耦。
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## 数据库集成
|
|
642
|
+
|
|
643
|
+
### TypeORM 集成
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
npm i @nestjs/typeorm typeorm pg
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
// app.module.ts
|
|
651
|
+
@Module({
|
|
652
|
+
imports: [
|
|
653
|
+
TypeOrmModule.forRootAsync({
|
|
654
|
+
imports: [ConfigModule],
|
|
655
|
+
inject: [ConfigService],
|
|
656
|
+
useFactory: (config: ConfigService) => ({
|
|
657
|
+
type: 'postgres',
|
|
658
|
+
host: config.get('DB_HOST'),
|
|
659
|
+
port: config.get<number>('DB_PORT', 5432),
|
|
660
|
+
username: config.get('DB_USER'),
|
|
661
|
+
password: config.get('DB_PASS'),
|
|
662
|
+
database: config.get('DB_NAME'),
|
|
663
|
+
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
664
|
+
synchronize: false, // 生产环境必须为 false
|
|
665
|
+
logging: config.get('NODE_ENV') === 'development',
|
|
666
|
+
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
|
667
|
+
}),
|
|
668
|
+
}),
|
|
669
|
+
],
|
|
670
|
+
})
|
|
671
|
+
export class AppModule {}
|
|
672
|
+
|
|
673
|
+
// user.entity.ts
|
|
674
|
+
@Entity('users')
|
|
675
|
+
export class User {
|
|
676
|
+
@PrimaryGeneratedColumn('uuid')
|
|
677
|
+
id: string;
|
|
678
|
+
|
|
679
|
+
@Column({ unique: true })
|
|
680
|
+
email: string;
|
|
681
|
+
|
|
682
|
+
@Column()
|
|
683
|
+
name: string;
|
|
684
|
+
|
|
685
|
+
@Column({ select: false })
|
|
686
|
+
password: string;
|
|
687
|
+
|
|
688
|
+
@OneToMany(() => Order, (order) => order.user)
|
|
689
|
+
orders: Order[];
|
|
690
|
+
|
|
691
|
+
@CreateDateColumn()
|
|
692
|
+
createdAt: Date;
|
|
693
|
+
|
|
694
|
+
@UpdateDateColumn()
|
|
695
|
+
updatedAt: Date;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// users.module.ts
|
|
699
|
+
@Module({
|
|
700
|
+
imports: [TypeOrmModule.forFeature([User])],
|
|
701
|
+
providers: [UsersService],
|
|
702
|
+
controllers: [UsersController],
|
|
703
|
+
})
|
|
704
|
+
export class UsersModule {}
|
|
705
|
+
|
|
706
|
+
// users.service.ts (Repository 模式)
|
|
707
|
+
@Injectable()
|
|
708
|
+
export class UsersService {
|
|
709
|
+
constructor(
|
|
710
|
+
@InjectRepository(User)
|
|
711
|
+
private readonly userRepo: Repository<User>,
|
|
712
|
+
) {}
|
|
713
|
+
|
|
714
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
715
|
+
return this.userRepo.findOne({
|
|
716
|
+
where: { email },
|
|
717
|
+
relations: ['orders'],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### TypeORM 迁移
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
# 生成迁移
|
|
727
|
+
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddUserTable
|
|
728
|
+
|
|
729
|
+
# 运行迁移
|
|
730
|
+
npx typeorm migration:run -d src/data-source.ts
|
|
731
|
+
|
|
732
|
+
# 回滚
|
|
733
|
+
npx typeorm migration:revert -d src/data-source.ts
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### TypeORM 事务
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
@Injectable()
|
|
740
|
+
export class OrdersService {
|
|
741
|
+
constructor(private readonly dataSource: DataSource) {}
|
|
742
|
+
|
|
743
|
+
async createOrder(userId: string, items: OrderItemDto[]): Promise<Order> {
|
|
744
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
745
|
+
await queryRunner.connect();
|
|
746
|
+
await queryRunner.startTransaction();
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
const order = queryRunner.manager.create(Order, { userId });
|
|
750
|
+
const savedOrder = await queryRunner.manager.save(order);
|
|
751
|
+
|
|
752
|
+
for (const item of items) {
|
|
753
|
+
// 扣减库存
|
|
754
|
+
const product = await queryRunner.manager.findOne(Product, {
|
|
755
|
+
where: { id: item.productId },
|
|
756
|
+
lock: { mode: 'pessimistic_write' },
|
|
757
|
+
});
|
|
758
|
+
if (!product || product.stock < item.quantity) {
|
|
759
|
+
throw new BadRequestException(`Insufficient stock for product ${item.productId}`);
|
|
760
|
+
}
|
|
761
|
+
product.stock -= item.quantity;
|
|
762
|
+
await queryRunner.manager.save(product);
|
|
763
|
+
|
|
764
|
+
// 创建订单项
|
|
765
|
+
const orderItem = queryRunner.manager.create(OrderItem, {
|
|
766
|
+
orderId: savedOrder.id, ...item,
|
|
767
|
+
});
|
|
768
|
+
await queryRunner.manager.save(orderItem);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
await queryRunner.commitTransaction();
|
|
772
|
+
return savedOrder;
|
|
773
|
+
} catch (err) {
|
|
774
|
+
await queryRunner.rollbackTransaction();
|
|
775
|
+
throw err;
|
|
776
|
+
} finally {
|
|
777
|
+
await queryRunner.release();
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Prisma 集成
|
|
784
|
+
|
|
785
|
+
```bash
|
|
786
|
+
npm i prisma @prisma/client
|
|
787
|
+
npx prisma init
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
```typescript
|
|
791
|
+
// prisma.service.ts
|
|
792
|
+
@Injectable()
|
|
793
|
+
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
|
794
|
+
async onModuleInit() {
|
|
795
|
+
await this.$connect();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async onModuleDestroy() {
|
|
799
|
+
await this.$disconnect();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 清理用于测试
|
|
803
|
+
async cleanDatabase() {
|
|
804
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
805
|
+
throw new Error('cleanDatabase is only for test environment');
|
|
806
|
+
}
|
|
807
|
+
const models = Reflect.ownKeys(this).filter((key) => typeof key === 'string' && !key.startsWith('_'));
|
|
808
|
+
return Promise.all(
|
|
809
|
+
models.map((model) => (this as any)[model]?.deleteMany?.()),
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// prisma.module.ts
|
|
815
|
+
@Global()
|
|
816
|
+
@Module({
|
|
817
|
+
providers: [PrismaService],
|
|
818
|
+
exports: [PrismaService],
|
|
819
|
+
})
|
|
820
|
+
export class PrismaModule {}
|
|
821
|
+
|
|
822
|
+
// users.service.ts (Prisma)
|
|
823
|
+
@Injectable()
|
|
824
|
+
export class UsersService {
|
|
825
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
826
|
+
|
|
827
|
+
async findAll(params: { skip?: number; take?: number; where?: Prisma.UserWhereInput }) {
|
|
828
|
+
const { skip, take, where } = params;
|
|
829
|
+
return this.prisma.user.findMany({ skip, take, where, include: { orders: true } });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Prisma 事务
|
|
833
|
+
async transferCredits(fromId: string, toId: string, amount: number) {
|
|
834
|
+
return this.prisma.$transaction(async (tx) => {
|
|
835
|
+
const sender = await tx.user.update({
|
|
836
|
+
where: { id: fromId },
|
|
837
|
+
data: { credits: { decrement: amount } },
|
|
838
|
+
});
|
|
839
|
+
if (sender.credits < 0) {
|
|
840
|
+
throw new BadRequestException('Insufficient credits');
|
|
841
|
+
}
|
|
842
|
+
await tx.user.update({
|
|
843
|
+
where: { id: toId },
|
|
844
|
+
data: { credits: { increment: amount } },
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## 认证与授权
|
|
854
|
+
|
|
855
|
+
### Passport + JWT 认证
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
npm i @nestjs/passport passport passport-local passport-jwt @nestjs/jwt
|
|
859
|
+
npm i -D @types/passport-local @types/passport-jwt
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
// auth.module.ts
|
|
864
|
+
@Module({
|
|
865
|
+
imports: [
|
|
866
|
+
UsersModule,
|
|
867
|
+
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
868
|
+
JwtModule.registerAsync({
|
|
869
|
+
imports: [ConfigModule],
|
|
870
|
+
inject: [ConfigService],
|
|
871
|
+
useFactory: (config: ConfigService) => ({
|
|
872
|
+
secret: config.get('JWT_SECRET'),
|
|
873
|
+
signOptions: { expiresIn: '15m' },
|
|
874
|
+
}),
|
|
875
|
+
}),
|
|
876
|
+
],
|
|
877
|
+
providers: [AuthService, LocalStrategy, JwtStrategy],
|
|
878
|
+
controllers: [AuthController],
|
|
879
|
+
exports: [AuthService],
|
|
880
|
+
})
|
|
881
|
+
export class AuthModule {}
|
|
882
|
+
|
|
883
|
+
// jwt.strategy.ts
|
|
884
|
+
@Injectable()
|
|
885
|
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
|
886
|
+
constructor(
|
|
887
|
+
private configService: ConfigService,
|
|
888
|
+
private usersService: UsersService,
|
|
889
|
+
) {
|
|
890
|
+
super({
|
|
891
|
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
892
|
+
ignoreExpiration: false,
|
|
893
|
+
secretOrKey: configService.get('JWT_SECRET'),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async validate(payload: JwtPayload): Promise<User> {
|
|
898
|
+
const user = await this.usersService.findOne(payload.sub);
|
|
899
|
+
if (!user) throw new UnauthorizedException();
|
|
900
|
+
return user;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// auth.service.ts
|
|
905
|
+
@Injectable()
|
|
906
|
+
export class AuthService {
|
|
907
|
+
constructor(
|
|
908
|
+
private usersService: UsersService,
|
|
909
|
+
private jwtService: JwtService,
|
|
910
|
+
) {}
|
|
911
|
+
|
|
912
|
+
async validateUser(email: string, password: string): Promise<User | null> {
|
|
913
|
+
const user = await this.usersService.findByEmail(email);
|
|
914
|
+
if (user && (await bcrypt.compare(password, user.password))) {
|
|
915
|
+
return user;
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async login(user: User) {
|
|
921
|
+
const payload: JwtPayload = { sub: user.id, email: user.email, roles: user.roles };
|
|
922
|
+
return {
|
|
923
|
+
accessToken: this.jwtService.sign(payload),
|
|
924
|
+
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
async refreshToken(token: string) {
|
|
929
|
+
try {
|
|
930
|
+
const payload = this.jwtService.verify(token);
|
|
931
|
+
const user = await this.usersService.findOne(payload.sub);
|
|
932
|
+
if (!user) throw new UnauthorizedException();
|
|
933
|
+
return this.login(user);
|
|
934
|
+
} catch {
|
|
935
|
+
throw new UnauthorizedException('Invalid refresh token');
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
### RBAC(基于角色的访问控制)
|
|
942
|
+
|
|
943
|
+
```typescript
|
|
944
|
+
export enum Role {
|
|
945
|
+
USER = 'user',
|
|
946
|
+
ADMIN = 'admin',
|
|
947
|
+
MODERATOR = 'moderator',
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// roles.decorator.ts
|
|
951
|
+
export const ROLES_KEY = 'roles';
|
|
952
|
+
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
|
953
|
+
|
|
954
|
+
// roles.guard.ts
|
|
955
|
+
@Injectable()
|
|
956
|
+
export class RolesGuard implements CanActivate {
|
|
957
|
+
constructor(private reflector: Reflector) {}
|
|
958
|
+
|
|
959
|
+
canActivate(context: ExecutionContext): boolean {
|
|
960
|
+
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
|
961
|
+
context.getHandler(),
|
|
962
|
+
context.getClass(),
|
|
963
|
+
]);
|
|
964
|
+
if (!requiredRoles) return true;
|
|
965
|
+
const { user } = context.switchToHttp().getRequest();
|
|
966
|
+
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// 使用
|
|
971
|
+
@Controller('posts')
|
|
972
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
973
|
+
export class PostsController {
|
|
974
|
+
@Delete(':id')
|
|
975
|
+
@Roles(Role.ADMIN, Role.MODERATOR)
|
|
976
|
+
remove(@Param('id') id: string) {
|
|
977
|
+
return this.postsService.remove(id);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### CASL 权限管理
|
|
983
|
+
|
|
984
|
+
```bash
|
|
985
|
+
npm i @casl/ability
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
// casl-ability.factory.ts
|
|
990
|
+
@Injectable()
|
|
991
|
+
export class CaslAbilityFactory {
|
|
992
|
+
createForUser(user: User) {
|
|
993
|
+
const { can, cannot, build } = new AbilityBuilder<Ability>(Ability);
|
|
994
|
+
|
|
995
|
+
if (user.roles.includes(Role.ADMIN)) {
|
|
996
|
+
can('manage', 'all');
|
|
997
|
+
} else {
|
|
998
|
+
can('read', 'Article');
|
|
999
|
+
can('create', 'Article');
|
|
1000
|
+
can('update', 'Article', { authorId: user.id }); // 只能编辑自己的文章
|
|
1001
|
+
cannot('delete', 'Article');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return build();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// policies.guard.ts
|
|
1009
|
+
@Injectable()
|
|
1010
|
+
export class PoliciesGuard implements CanActivate {
|
|
1011
|
+
constructor(
|
|
1012
|
+
private reflector: Reflector,
|
|
1013
|
+
private caslAbilityFactory: CaslAbilityFactory,
|
|
1014
|
+
) {}
|
|
1015
|
+
|
|
1016
|
+
canActivate(context: ExecutionContext): boolean {
|
|
1017
|
+
const policyHandlers = this.reflector.get<PolicyHandler[]>('check_policies', context.getHandler()) || [];
|
|
1018
|
+
const { user } = context.switchToHttp().getRequest();
|
|
1019
|
+
const ability = this.caslAbilityFactory.createForUser(user);
|
|
1020
|
+
return policyHandlers.every((handler) => handler(ability));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
---
|
|
1026
|
+
|
|
1027
|
+
## WebSocket
|
|
1028
|
+
|
|
1029
|
+
### Gateway 基础
|
|
1030
|
+
|
|
1031
|
+
```bash
|
|
1032
|
+
npm i @nestjs/websockets @nestjs/platform-socket.io socket.io
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
import {
|
|
1037
|
+
WebSocketGateway, WebSocketServer,
|
|
1038
|
+
SubscribeMessage, MessageBody,
|
|
1039
|
+
ConnectedSocket, OnGatewayInit,
|
|
1040
|
+
OnGatewayConnection, OnGatewayDisconnect,
|
|
1041
|
+
WsException,
|
|
1042
|
+
} from '@nestjs/websockets';
|
|
1043
|
+
import { Server, Socket } from 'socket.io';
|
|
1044
|
+
|
|
1045
|
+
@WebSocketGateway({
|
|
1046
|
+
cors: { origin: '*' },
|
|
1047
|
+
namespace: '/chat',
|
|
1048
|
+
})
|
|
1049
|
+
export class ChatGateway
|
|
1050
|
+
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
|
|
1051
|
+
{
|
|
1052
|
+
@WebSocketServer()
|
|
1053
|
+
server: Server;
|
|
1054
|
+
|
|
1055
|
+
private readonly logger = new Logger(ChatGateway.name);
|
|
1056
|
+
|
|
1057
|
+
afterInit(server: Server) {
|
|
1058
|
+
this.logger.log('WebSocket Gateway initialized');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
handleConnection(client: Socket) {
|
|
1062
|
+
this.logger.log(`Client connected: ${client.id}`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
handleDisconnect(client: Socket) {
|
|
1066
|
+
this.logger.log(`Client disconnected: ${client.id}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
@SubscribeMessage('sendMessage')
|
|
1070
|
+
handleMessage(
|
|
1071
|
+
@MessageBody() data: { room: string; message: string },
|
|
1072
|
+
@ConnectedSocket() client: Socket,
|
|
1073
|
+
) {
|
|
1074
|
+
// 向房间广播(不包括发送者)
|
|
1075
|
+
client.to(data.room).emit('newMessage', {
|
|
1076
|
+
sender: client.id,
|
|
1077
|
+
message: data.message,
|
|
1078
|
+
timestamp: new Date(),
|
|
1079
|
+
});
|
|
1080
|
+
return { event: 'messageSent', data: { success: true } };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
@SubscribeMessage('joinRoom')
|
|
1084
|
+
handleJoinRoom(
|
|
1085
|
+
@MessageBody() room: string,
|
|
1086
|
+
@ConnectedSocket() client: Socket,
|
|
1087
|
+
) {
|
|
1088
|
+
client.join(room);
|
|
1089
|
+
this.server.to(room).emit('userJoined', { userId: client.id });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@SubscribeMessage('leaveRoom')
|
|
1093
|
+
handleLeaveRoom(
|
|
1094
|
+
@MessageBody() room: string,
|
|
1095
|
+
@ConnectedSocket() client: Socket,
|
|
1096
|
+
) {
|
|
1097
|
+
client.leave(room);
|
|
1098
|
+
this.server.to(room).emit('userLeft', { userId: client.id });
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### WebSocket 认证
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
@WebSocketGateway()
|
|
1107
|
+
export class AuthenticatedGateway {
|
|
1108
|
+
@WebSocketServer() server: Server;
|
|
1109
|
+
|
|
1110
|
+
afterInit(server: Server) {
|
|
1111
|
+
server.use(async (socket: Socket, next) => {
|
|
1112
|
+
try {
|
|
1113
|
+
const token = socket.handshake.auth.token
|
|
1114
|
+
|| socket.handshake.headers.authorization?.split(' ')[1];
|
|
1115
|
+
if (!token) throw new WsException('Missing token');
|
|
1116
|
+
const payload = this.jwtService.verify(token);
|
|
1117
|
+
socket.data.user = payload;
|
|
1118
|
+
next();
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
next(new Error('Unauthorized'));
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
---
|
|
1128
|
+
|
|
1129
|
+
## 微服务
|
|
1130
|
+
|
|
1131
|
+
### Transport 层概览
|
|
1132
|
+
|
|
1133
|
+
NestJS 微服务抽象了通信层,支持多种传输协议:
|
|
1134
|
+
|
|
1135
|
+
```typescript
|
|
1136
|
+
// TCP(默认)
|
|
1137
|
+
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
|
|
1138
|
+
transport: Transport.TCP,
|
|
1139
|
+
options: { host: '0.0.0.0', port: 3001 },
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Redis
|
|
1143
|
+
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
|
|
1144
|
+
transport: Transport.REDIS,
|
|
1145
|
+
options: { host: 'localhost', port: 6379 },
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// Kafka
|
|
1149
|
+
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
|
|
1150
|
+
transport: Transport.KAFKA,
|
|
1151
|
+
options: {
|
|
1152
|
+
client: { brokers: ['localhost:9092'], clientId: 'orders-service' },
|
|
1153
|
+
consumer: { groupId: 'orders-consumer' },
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// gRPC
|
|
1158
|
+
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
|
|
1159
|
+
transport: Transport.GRPC,
|
|
1160
|
+
options: {
|
|
1161
|
+
package: 'orders',
|
|
1162
|
+
protoPath: join(__dirname, 'orders.proto'),
|
|
1163
|
+
url: '0.0.0.0:50051',
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### 消息模式与事件
|
|
1169
|
+
|
|
1170
|
+
```typescript
|
|
1171
|
+
// 微服务端 — 处理消息
|
|
1172
|
+
@Controller()
|
|
1173
|
+
export class OrdersController {
|
|
1174
|
+
// 请求-响应模式(同步通信)
|
|
1175
|
+
@MessagePattern({ cmd: 'get_order' })
|
|
1176
|
+
getOrder(@Payload() data: { orderId: string }) {
|
|
1177
|
+
return this.ordersService.findOne(data.orderId);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// 事件模式(异步通信,无回复)
|
|
1181
|
+
@EventPattern('order_created')
|
|
1182
|
+
handleOrderCreated(@Payload() data: OrderCreatedEvent) {
|
|
1183
|
+
this.notificationService.sendOrderConfirmation(data);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// 客户端 — 发送消息
|
|
1188
|
+
@Module({
|
|
1189
|
+
imports: [
|
|
1190
|
+
ClientsModule.register([{
|
|
1191
|
+
name: 'ORDERS_SERVICE',
|
|
1192
|
+
transport: Transport.REDIS,
|
|
1193
|
+
options: { host: 'localhost', port: 6379 },
|
|
1194
|
+
}]),
|
|
1195
|
+
],
|
|
1196
|
+
})
|
|
1197
|
+
export class ApiGatewayModule {}
|
|
1198
|
+
|
|
1199
|
+
@Injectable()
|
|
1200
|
+
export class ApiGatewayService {
|
|
1201
|
+
constructor(@Inject('ORDERS_SERVICE') private ordersClient: ClientProxy) {}
|
|
1202
|
+
|
|
1203
|
+
getOrder(orderId: string): Observable<Order> {
|
|
1204
|
+
return this.ordersClient.send({ cmd: 'get_order' }, { orderId });
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
createOrder(dto: CreateOrderDto) {
|
|
1208
|
+
this.ordersClient.emit('order_created', dto); // 单向事件
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### 混合应用(HTTP + 微服务)
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
async function bootstrap() {
|
|
1217
|
+
const app = await NestFactory.create(AppModule);
|
|
1218
|
+
|
|
1219
|
+
// 同时连接 Redis 和 Kafka 微服务
|
|
1220
|
+
app.connectMicroservice<MicroserviceOptions>({
|
|
1221
|
+
transport: Transport.REDIS,
|
|
1222
|
+
options: { host: 'localhost', port: 6379 },
|
|
1223
|
+
});
|
|
1224
|
+
app.connectMicroservice<MicroserviceOptions>({
|
|
1225
|
+
transport: Transport.KAFKA,
|
|
1226
|
+
options: {
|
|
1227
|
+
client: { brokers: ['localhost:9092'] },
|
|
1228
|
+
consumer: { groupId: 'hybrid-consumer' },
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
await app.startAllMicroservices();
|
|
1233
|
+
await app.listen(3000);
|
|
1234
|
+
}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
---
|
|
1238
|
+
|
|
1239
|
+
## GraphQL
|
|
1240
|
+
|
|
1241
|
+
### Code-First 方式
|
|
1242
|
+
|
|
1243
|
+
```bash
|
|
1244
|
+
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
```typescript
|
|
1248
|
+
// app.module.ts
|
|
1249
|
+
@Module({
|
|
1250
|
+
imports: [
|
|
1251
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
1252
|
+
driver: ApolloDriver,
|
|
1253
|
+
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
|
1254
|
+
sortSchema: true,
|
|
1255
|
+
playground: process.env.NODE_ENV !== 'production',
|
|
1256
|
+
context: ({ req }) => ({ req }),
|
|
1257
|
+
}),
|
|
1258
|
+
],
|
|
1259
|
+
})
|
|
1260
|
+
export class AppModule {}
|
|
1261
|
+
|
|
1262
|
+
// user.model.ts
|
|
1263
|
+
@ObjectType()
|
|
1264
|
+
export class UserModel {
|
|
1265
|
+
@Field(() => ID)
|
|
1266
|
+
id: string;
|
|
1267
|
+
|
|
1268
|
+
@Field()
|
|
1269
|
+
email: string;
|
|
1270
|
+
|
|
1271
|
+
@Field()
|
|
1272
|
+
name: string;
|
|
1273
|
+
|
|
1274
|
+
@Field(() => [OrderModel], { nullable: true })
|
|
1275
|
+
orders?: OrderModel[];
|
|
1276
|
+
|
|
1277
|
+
@Field()
|
|
1278
|
+
createdAt: Date;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// users.resolver.ts
|
|
1282
|
+
@Resolver(() => UserModel)
|
|
1283
|
+
export class UsersResolver {
|
|
1284
|
+
constructor(private readonly usersService: UsersService) {}
|
|
1285
|
+
|
|
1286
|
+
@Query(() => [UserModel], { name: 'users' })
|
|
1287
|
+
findAll(
|
|
1288
|
+
@Args('page', { type: () => Int, defaultValue: 1 }) page: number,
|
|
1289
|
+
) {
|
|
1290
|
+
return this.usersService.findAll(page);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
@Query(() => UserModel, { name: 'user' })
|
|
1294
|
+
findOne(@Args('id', { type: () => ID }) id: string) {
|
|
1295
|
+
return this.usersService.findOne(id);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
@Mutation(() => UserModel)
|
|
1299
|
+
createUser(@Args('input') input: CreateUserInput) {
|
|
1300
|
+
return this.usersService.create(input);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
@ResolveField(() => [OrderModel])
|
|
1304
|
+
orders(@Parent() user: UserModel) {
|
|
1305
|
+
return this.ordersService.findByUserId(user.id);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
### DataLoader(N+1 问题解决)
|
|
1311
|
+
|
|
1312
|
+
```typescript
|
|
1313
|
+
import DataLoader from 'dataloader';
|
|
1314
|
+
|
|
1315
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
1316
|
+
export class OrdersLoader {
|
|
1317
|
+
constructor(private readonly ordersService: OrdersService) {}
|
|
1318
|
+
|
|
1319
|
+
readonly batchByUserId = new DataLoader<string, Order[]>(async (userIds: string[]) => {
|
|
1320
|
+
const orders = await this.ordersService.findByUserIds([...userIds]);
|
|
1321
|
+
const ordersMap = new Map<string, Order[]>();
|
|
1322
|
+
orders.forEach((order) => {
|
|
1323
|
+
const existing = ordersMap.get(order.userId) || [];
|
|
1324
|
+
existing.push(order);
|
|
1325
|
+
ordersMap.set(order.userId, existing);
|
|
1326
|
+
});
|
|
1327
|
+
return userIds.map((id) => ordersMap.get(id) || []);
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// resolver 中使用
|
|
1332
|
+
@ResolveField(() => [OrderModel])
|
|
1333
|
+
orders(@Parent() user: UserModel) {
|
|
1334
|
+
return this.ordersLoader.batchByUserId.load(user.id);
|
|
1335
|
+
}
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
### 订阅 (Subscriptions)
|
|
1339
|
+
|
|
1340
|
+
```typescript
|
|
1341
|
+
// app.module.ts 启用订阅
|
|
1342
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
1343
|
+
driver: ApolloDriver,
|
|
1344
|
+
autoSchemaFile: true,
|
|
1345
|
+
subscriptions: {
|
|
1346
|
+
'graphql-ws': true, // 推荐:graphql-ws 协议
|
|
1347
|
+
'subscriptions-transport-ws': false,
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// resolver
|
|
1352
|
+
@Resolver()
|
|
1353
|
+
export class MessagesResolver {
|
|
1354
|
+
@Subscription(() => MessageModel, {
|
|
1355
|
+
filter: (payload, variables) =>
|
|
1356
|
+
payload.messageAdded.channelId === variables.channelId,
|
|
1357
|
+
})
|
|
1358
|
+
messageAdded(@Args('channelId') channelId: string) {
|
|
1359
|
+
return pubSub.asyncIterator('messageAdded');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
@Mutation(() => MessageModel)
|
|
1363
|
+
async sendMessage(@Args('input') input: SendMessageInput) {
|
|
1364
|
+
const message = await this.messagesService.create(input);
|
|
1365
|
+
pubSub.publish('messageAdded', { messageAdded: message });
|
|
1366
|
+
return message;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
## 队列处理
|
|
1374
|
+
|
|
1375
|
+
### Bull / BullMQ 集成
|
|
1376
|
+
|
|
1377
|
+
```bash
|
|
1378
|
+
npm i @nestjs/bullmq bullmq
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
```typescript
|
|
1382
|
+
// app.module.ts
|
|
1383
|
+
@Module({
|
|
1384
|
+
imports: [
|
|
1385
|
+
BullModule.forRoot({
|
|
1386
|
+
connection: { host: 'localhost', port: 6379 },
|
|
1387
|
+
}),
|
|
1388
|
+
BullModule.registerQueue(
|
|
1389
|
+
{ name: 'email' },
|
|
1390
|
+
{ name: 'report' },
|
|
1391
|
+
),
|
|
1392
|
+
],
|
|
1393
|
+
})
|
|
1394
|
+
export class AppModule {}
|
|
1395
|
+
|
|
1396
|
+
// email.producer.ts
|
|
1397
|
+
@Injectable()
|
|
1398
|
+
export class EmailService {
|
|
1399
|
+
constructor(@InjectQueue('email') private emailQueue: Queue) {}
|
|
1400
|
+
|
|
1401
|
+
async sendWelcomeEmail(userId: string) {
|
|
1402
|
+
await this.emailQueue.add('welcome', { userId }, {
|
|
1403
|
+
delay: 5000, // 延迟 5 秒发送
|
|
1404
|
+
attempts: 3, // 失败重试 3 次
|
|
1405
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
1406
|
+
removeOnComplete: 100, // 保留最近 100 个完成任务
|
|
1407
|
+
removeOnFail: 500, // 保留最近 500 个失败任务
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async sendBulkEmails(userIds: string[]) {
|
|
1412
|
+
const jobs = userIds.map((userId) => ({
|
|
1413
|
+
name: 'bulk',
|
|
1414
|
+
data: { userId },
|
|
1415
|
+
opts: { priority: 10 },
|
|
1416
|
+
}));
|
|
1417
|
+
await this.emailQueue.addBulk(jobs);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// email.consumer.ts
|
|
1422
|
+
@Processor('email')
|
|
1423
|
+
export class EmailConsumer extends WorkerHost {
|
|
1424
|
+
private readonly logger = new Logger(EmailConsumer.name);
|
|
1425
|
+
|
|
1426
|
+
async process(job: Job<{ userId: string }>): Promise<void> {
|
|
1427
|
+
switch (job.name) {
|
|
1428
|
+
case 'welcome':
|
|
1429
|
+
await this.sendWelcomeEmail(job.data.userId);
|
|
1430
|
+
break;
|
|
1431
|
+
case 'bulk':
|
|
1432
|
+
await this.sendBulkEmail(job.data.userId);
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
@OnWorkerEvent('completed')
|
|
1438
|
+
onCompleted(job: Job) {
|
|
1439
|
+
this.logger.log(`Job ${job.id} completed`);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
@OnWorkerEvent('failed')
|
|
1443
|
+
onFailed(job: Job, error: Error) {
|
|
1444
|
+
this.logger.error(`Job ${job.id} failed: ${error.message}`);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
private async sendWelcomeEmail(userId: string) {
|
|
1448
|
+
// 实际发送逻辑
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private async sendBulkEmail(userId: string) {
|
|
1452
|
+
// 批量发送逻辑
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
## 缓存
|
|
1460
|
+
|
|
1461
|
+
### Redis 缓存集成
|
|
1462
|
+
|
|
1463
|
+
```bash
|
|
1464
|
+
npm i @nestjs/cache-manager cache-manager cache-manager-ioredis-yet ioredis
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
```typescript
|
|
1468
|
+
// app.module.ts
|
|
1469
|
+
@Module({
|
|
1470
|
+
imports: [
|
|
1471
|
+
CacheModule.registerAsync({
|
|
1472
|
+
isGlobal: true,
|
|
1473
|
+
imports: [ConfigModule],
|
|
1474
|
+
inject: [ConfigService],
|
|
1475
|
+
useFactory: (config: ConfigService) => ({
|
|
1476
|
+
store: redisStore,
|
|
1477
|
+
host: config.get('REDIS_HOST', 'localhost'),
|
|
1478
|
+
port: config.get('REDIS_PORT', 6379),
|
|
1479
|
+
ttl: 300, // 默认 5 分钟
|
|
1480
|
+
}),
|
|
1481
|
+
}),
|
|
1482
|
+
],
|
|
1483
|
+
})
|
|
1484
|
+
export class AppModule {}
|
|
1485
|
+
|
|
1486
|
+
// 使用 CacheInterceptor(自动缓存 GET 请求)
|
|
1487
|
+
@Controller('products')
|
|
1488
|
+
@UseInterceptors(CacheInterceptor)
|
|
1489
|
+
export class ProductsController {
|
|
1490
|
+
@Get()
|
|
1491
|
+
@CacheTTL(600) // 覆盖默认 TTL,10 分钟
|
|
1492
|
+
@CacheKey('all_products')
|
|
1493
|
+
findAll() {
|
|
1494
|
+
return this.productsService.findAll();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// 手动缓存控制
|
|
1499
|
+
@Injectable()
|
|
1500
|
+
export class ProductsService {
|
|
1501
|
+
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
|
1502
|
+
|
|
1503
|
+
async findOne(id: string): Promise<Product> {
|
|
1504
|
+
const cacheKey = `product:${id}`;
|
|
1505
|
+
const cached = await this.cacheManager.get<Product>(cacheKey);
|
|
1506
|
+
if (cached) return cached;
|
|
1507
|
+
|
|
1508
|
+
const product = await this.productRepo.findOne({ where: { id } });
|
|
1509
|
+
if (product) {
|
|
1510
|
+
await this.cacheManager.set(cacheKey, product, 600);
|
|
1511
|
+
}
|
|
1512
|
+
return product;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async update(id: string, dto: UpdateProductDto): Promise<Product> {
|
|
1516
|
+
const product = await this.productRepo.save({ id, ...dto });
|
|
1517
|
+
await this.cacheManager.del(`product:${id}`); // 失效缓存
|
|
1518
|
+
await this.cacheManager.del('all_products');
|
|
1519
|
+
return product;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
## 测试
|
|
1527
|
+
|
|
1528
|
+
### 单元测试
|
|
1529
|
+
|
|
1530
|
+
```typescript
|
|
1531
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
1532
|
+
|
|
1533
|
+
describe('UsersService', () => {
|
|
1534
|
+
let service: UsersService;
|
|
1535
|
+
let repo: Repository<User>;
|
|
1536
|
+
|
|
1537
|
+
beforeEach(async () => {
|
|
1538
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
1539
|
+
providers: [
|
|
1540
|
+
UsersService,
|
|
1541
|
+
{
|
|
1542
|
+
provide: getRepositoryToken(User),
|
|
1543
|
+
useValue: {
|
|
1544
|
+
findOne: jest.fn(),
|
|
1545
|
+
find: jest.fn(),
|
|
1546
|
+
save: jest.fn(),
|
|
1547
|
+
create: jest.fn(),
|
|
1548
|
+
delete: jest.fn(),
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
],
|
|
1552
|
+
}).compile();
|
|
1553
|
+
|
|
1554
|
+
service = module.get<UsersService>(UsersService);
|
|
1555
|
+
repo = module.get<Repository<User>>(getRepositoryToken(User));
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
describe('findOne', () => {
|
|
1559
|
+
it('should return a user', async () => {
|
|
1560
|
+
const user = { id: '1', name: 'Alice', email: 'alice@test.com' };
|
|
1561
|
+
jest.spyOn(repo, 'findOne').mockResolvedValue(user as User);
|
|
1562
|
+
|
|
1563
|
+
const result = await service.findOne('1');
|
|
1564
|
+
expect(result).toEqual(user);
|
|
1565
|
+
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
it('should throw NotFoundException if user not found', async () => {
|
|
1569
|
+
jest.spyOn(repo, 'findOne').mockResolvedValue(null);
|
|
1570
|
+
|
|
1571
|
+
await expect(service.findOne('999')).rejects.toThrow(NotFoundException);
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
### E2E 测试
|
|
1578
|
+
|
|
1579
|
+
```typescript
|
|
1580
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
1581
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
1582
|
+
import * as request from 'supertest';
|
|
1583
|
+
|
|
1584
|
+
describe('UsersController (e2e)', () => {
|
|
1585
|
+
let app: INestApplication;
|
|
1586
|
+
|
|
1587
|
+
beforeAll(async () => {
|
|
1588
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
1589
|
+
imports: [AppModule],
|
|
1590
|
+
})
|
|
1591
|
+
.overrideProvider(UsersService)
|
|
1592
|
+
.useValue({
|
|
1593
|
+
findAll: jest.fn().mockResolvedValue([{ id: '1', name: 'Alice' }]),
|
|
1594
|
+
findOne: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
|
|
1595
|
+
create: jest.fn().mockResolvedValue({ id: '2', name: 'Bob' }),
|
|
1596
|
+
})
|
|
1597
|
+
.compile();
|
|
1598
|
+
|
|
1599
|
+
app = moduleFixture.createNestApplication();
|
|
1600
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
|
1601
|
+
await app.init();
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
afterAll(async () => {
|
|
1605
|
+
await app.close();
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
describe('GET /users', () => {
|
|
1609
|
+
it('should return an array of users', () => {
|
|
1610
|
+
return request(app.getHttpServer())
|
|
1611
|
+
.get('/users')
|
|
1612
|
+
.expect(200)
|
|
1613
|
+
.expect((res) => {
|
|
1614
|
+
expect(res.body).toHaveLength(1);
|
|
1615
|
+
expect(res.body[0].name).toBe('Alice');
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
describe('POST /users', () => {
|
|
1621
|
+
it('should create a user', () => {
|
|
1622
|
+
return request(app.getHttpServer())
|
|
1623
|
+
.post('/users')
|
|
1624
|
+
.send({ name: 'Bob', email: 'bob@test.com', password: 'securePass1' })
|
|
1625
|
+
.expect(201);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it('should reject invalid input', () => {
|
|
1629
|
+
return request(app.getHttpServer())
|
|
1630
|
+
.post('/users')
|
|
1631
|
+
.send({ name: '' })
|
|
1632
|
+
.expect(400);
|
|
1633
|
+
});
|
|
1634
|
+
});
|
|
1635
|
+
});
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
### Testing 模块高级用法
|
|
1639
|
+
|
|
1640
|
+
```typescript
|
|
1641
|
+
// 自动 Mock 所有依赖
|
|
1642
|
+
const module = await Test.createTestingModule({
|
|
1643
|
+
providers: [UsersService],
|
|
1644
|
+
})
|
|
1645
|
+
.useMocker((token) => {
|
|
1646
|
+
if (typeof token === 'function') {
|
|
1647
|
+
return createMock(token); // 使用 @golevelup/ts-jest
|
|
1648
|
+
}
|
|
1649
|
+
return {};
|
|
1650
|
+
})
|
|
1651
|
+
.compile();
|
|
1652
|
+
|
|
1653
|
+
// 覆盖守卫
|
|
1654
|
+
const module = await Test.createTestingModule({
|
|
1655
|
+
imports: [AppModule],
|
|
1656
|
+
})
|
|
1657
|
+
.overrideGuard(JwtAuthGuard)
|
|
1658
|
+
.useValue({ canActivate: () => true })
|
|
1659
|
+
.compile();
|
|
1660
|
+
|
|
1661
|
+
// 覆盖拦截器
|
|
1662
|
+
const module = await Test.createTestingModule({
|
|
1663
|
+
imports: [AppModule],
|
|
1664
|
+
})
|
|
1665
|
+
.overrideInterceptor(CacheInterceptor)
|
|
1666
|
+
.useValue({ intercept: (_, next) => next.handle() })
|
|
1667
|
+
.compile();
|
|
1668
|
+
```
|
|
1669
|
+
|
|
1670
|
+
---
|
|
1671
|
+
|
|
1672
|
+
## 部署
|
|
1673
|
+
|
|
1674
|
+
### Docker
|
|
1675
|
+
|
|
1676
|
+
```dockerfile
|
|
1677
|
+
# 多阶段构建
|
|
1678
|
+
FROM node:20-alpine AS builder
|
|
1679
|
+
WORKDIR /app
|
|
1680
|
+
COPY package*.json ./
|
|
1681
|
+
RUN npm ci
|
|
1682
|
+
COPY . .
|
|
1683
|
+
RUN npm run build
|
|
1684
|
+
|
|
1685
|
+
FROM node:20-alpine AS production
|
|
1686
|
+
WORKDIR /app
|
|
1687
|
+
COPY package*.json ./
|
|
1688
|
+
RUN npm ci --only=production && npm cache clean --force
|
|
1689
|
+
COPY --from=builder /app/dist ./dist
|
|
1690
|
+
|
|
1691
|
+
# 非 root 用户运行
|
|
1692
|
+
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
|
|
1693
|
+
USER nestjs
|
|
1694
|
+
|
|
1695
|
+
EXPOSE 3000
|
|
1696
|
+
CMD ["node", "dist/main"]
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
### Kubernetes
|
|
1700
|
+
|
|
1701
|
+
```yaml
|
|
1702
|
+
apiVersion: apps/v1
|
|
1703
|
+
kind: Deployment
|
|
1704
|
+
metadata:
|
|
1705
|
+
name: nestjs-api
|
|
1706
|
+
spec:
|
|
1707
|
+
replicas: 3
|
|
1708
|
+
selector:
|
|
1709
|
+
matchLabels:
|
|
1710
|
+
app: nestjs-api
|
|
1711
|
+
template:
|
|
1712
|
+
metadata:
|
|
1713
|
+
labels:
|
|
1714
|
+
app: nestjs-api
|
|
1715
|
+
spec:
|
|
1716
|
+
containers:
|
|
1717
|
+
- name: api
|
|
1718
|
+
image: registry.example.com/nestjs-api:latest
|
|
1719
|
+
ports:
|
|
1720
|
+
- containerPort: 3000
|
|
1721
|
+
env:
|
|
1722
|
+
- name: NODE_ENV
|
|
1723
|
+
value: "production"
|
|
1724
|
+
- name: DATABASE_URL
|
|
1725
|
+
valueFrom:
|
|
1726
|
+
secretKeyRef:
|
|
1727
|
+
name: db-secrets
|
|
1728
|
+
key: url
|
|
1729
|
+
resources:
|
|
1730
|
+
requests:
|
|
1731
|
+
cpu: 100m
|
|
1732
|
+
memory: 256Mi
|
|
1733
|
+
limits:
|
|
1734
|
+
cpu: 500m
|
|
1735
|
+
memory: 512Mi
|
|
1736
|
+
livenessProbe:
|
|
1737
|
+
httpGet:
|
|
1738
|
+
path: /health
|
|
1739
|
+
port: 3000
|
|
1740
|
+
initialDelaySeconds: 30
|
|
1741
|
+
periodSeconds: 10
|
|
1742
|
+
readinessProbe:
|
|
1743
|
+
httpGet:
|
|
1744
|
+
path: /health
|
|
1745
|
+
port: 3000
|
|
1746
|
+
initialDelaySeconds: 5
|
|
1747
|
+
periodSeconds: 5
|
|
1748
|
+
---
|
|
1749
|
+
apiVersion: v1
|
|
1750
|
+
kind: Service
|
|
1751
|
+
metadata:
|
|
1752
|
+
name: nestjs-api
|
|
1753
|
+
spec:
|
|
1754
|
+
selector:
|
|
1755
|
+
app: nestjs-api
|
|
1756
|
+
ports:
|
|
1757
|
+
- port: 80
|
|
1758
|
+
targetPort: 3000
|
|
1759
|
+
type: ClusterIP
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
### 健康检查
|
|
1763
|
+
|
|
1764
|
+
```bash
|
|
1765
|
+
npm i @nestjs/terminus
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
```typescript
|
|
1769
|
+
@Controller('health')
|
|
1770
|
+
export class HealthController {
|
|
1771
|
+
constructor(
|
|
1772
|
+
private health: HealthCheckService,
|
|
1773
|
+
private db: TypeOrmHealthIndicator,
|
|
1774
|
+
private redis: MicroserviceHealthIndicator,
|
|
1775
|
+
private memory: MemoryHealthIndicator,
|
|
1776
|
+
private disk: DiskHealthIndicator,
|
|
1777
|
+
) {}
|
|
1778
|
+
|
|
1779
|
+
@Get()
|
|
1780
|
+
check() {
|
|
1781
|
+
return this.health.check([
|
|
1782
|
+
() => this.db.pingCheck('database'),
|
|
1783
|
+
() => this.redis.pingCheck('redis', {
|
|
1784
|
+
transport: Transport.REDIS,
|
|
1785
|
+
options: { host: 'localhost', port: 6379 },
|
|
1786
|
+
}),
|
|
1787
|
+
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB
|
|
1788
|
+
() => this.disk.checkStorage('disk', { thresholdPercent: 0.9, path: '/' }),
|
|
1789
|
+
]);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
### Monorepo 结构
|
|
1795
|
+
|
|
1796
|
+
```bash
|
|
1797
|
+
nest generate app orders-service # 添加子应用
|
|
1798
|
+
nest generate lib shared # 添加共享库
|
|
1799
|
+
```
|
|
1800
|
+
|
|
1801
|
+
```
|
|
1802
|
+
project-root/
|
|
1803
|
+
├── apps/
|
|
1804
|
+
│ ├── api-gateway/ # HTTP 入口
|
|
1805
|
+
│ │ └── src/
|
|
1806
|
+
│ ├── orders-service/ # 订单微服务
|
|
1807
|
+
│ │ └── src/
|
|
1808
|
+
│ └── notifications-service/ # 通知微服务
|
|
1809
|
+
│ └── src/
|
|
1810
|
+
├── libs/
|
|
1811
|
+
│ ├── shared/ # 共享 DTO / 接口
|
|
1812
|
+
│ │ └── src/
|
|
1813
|
+
│ └── database/ # 共享数据库模块
|
|
1814
|
+
│ └── src/
|
|
1815
|
+
├── nest-cli.json
|
|
1816
|
+
└── tsconfig.json
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
### Serverless 部署
|
|
1820
|
+
|
|
1821
|
+
```typescript
|
|
1822
|
+
// serverless.ts (AWS Lambda)
|
|
1823
|
+
import { NestFactory } from '@nestjs/core';
|
|
1824
|
+
import { ExpressAdapter } from '@nestjs/platform-express';
|
|
1825
|
+
import serverlessExpress from '@codegenie/serverless-express';
|
|
1826
|
+
import express from 'express';
|
|
1827
|
+
|
|
1828
|
+
let cachedServer: any;
|
|
1829
|
+
|
|
1830
|
+
async function bootstrap() {
|
|
1831
|
+
if (cachedServer) return cachedServer;
|
|
1832
|
+
const expressApp = express();
|
|
1833
|
+
const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
|
|
1834
|
+
app.enableCors();
|
|
1835
|
+
await app.init();
|
|
1836
|
+
cachedServer = serverlessExpress({ app: expressApp });
|
|
1837
|
+
return cachedServer;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
export const handler = async (event: any, context: any, callback: any) => {
|
|
1841
|
+
const server = await bootstrap();
|
|
1842
|
+
return server(event, context, callback);
|
|
1843
|
+
};
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
---
|
|
1847
|
+
|
|
1848
|
+
## 性能优化
|
|
1849
|
+
|
|
1850
|
+
### 1. Fastify 适配器
|
|
1851
|
+
|
|
1852
|
+
```bash
|
|
1853
|
+
npm i @nestjs/platform-fastify
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
```typescript
|
|
1857
|
+
import { NestFactory } from '@nestjs/core';
|
|
1858
|
+
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
1859
|
+
|
|
1860
|
+
async function bootstrap() {
|
|
1861
|
+
const app = await NestFactory.create<NestFastifyApplication>(
|
|
1862
|
+
AppModule,
|
|
1863
|
+
new FastifyAdapter({ logger: true }),
|
|
1864
|
+
);
|
|
1865
|
+
// 注意:Fastify 需要 listen 绑定 0.0.0.0(Docker/K8s 中必需)
|
|
1866
|
+
await app.listen(3000, '0.0.0.0');
|
|
1867
|
+
}
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
> **基准参考**: Fastify 适配器在简单 JSON 返回场景下吞吐量约为 Express 的 2x。实际业务中差距缩小,但在高并发场景仍有明显优势。
|
|
1871
|
+
|
|
1872
|
+
### 2. 压缩
|
|
1873
|
+
|
|
1874
|
+
```bash
|
|
1875
|
+
npm i @nestjs/platform-express compression
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
```typescript
|
|
1879
|
+
import compression from 'compression';
|
|
1880
|
+
|
|
1881
|
+
async function bootstrap() {
|
|
1882
|
+
const app = await NestFactory.create(AppModule);
|
|
1883
|
+
app.use(compression({
|
|
1884
|
+
threshold: 1024, // 仅压缩大于 1KB 的响应
|
|
1885
|
+
level: 6,
|
|
1886
|
+
}));
|
|
1887
|
+
await app.listen(3000);
|
|
1888
|
+
}
|
|
1889
|
+
```
|
|
1890
|
+
|
|
1891
|
+
### 3. 懒加载模块
|
|
1892
|
+
|
|
1893
|
+
```typescript
|
|
1894
|
+
import { LazyModuleLoader } from '@nestjs/core';
|
|
1895
|
+
|
|
1896
|
+
@Injectable()
|
|
1897
|
+
export class ReportService {
|
|
1898
|
+
constructor(private lazyModuleLoader: LazyModuleLoader) {}
|
|
1899
|
+
|
|
1900
|
+
async generateReport() {
|
|
1901
|
+
// 仅在需要时加载重量级模块
|
|
1902
|
+
const { ReportModule } = await import('./report.module');
|
|
1903
|
+
const moduleRef = await this.lazyModuleLoader.load(() => ReportModule);
|
|
1904
|
+
const reportGenerator = moduleRef.get(ReportGenerator);
|
|
1905
|
+
return reportGenerator.generate();
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1910
|
+
### 4. 集群模式
|
|
1911
|
+
|
|
1912
|
+
```typescript
|
|
1913
|
+
// main.ts
|
|
1914
|
+
import cluster from 'node:cluster';
|
|
1915
|
+
import os from 'node:os';
|
|
1916
|
+
|
|
1917
|
+
async function bootstrap() {
|
|
1918
|
+
const app = await NestFactory.create(AppModule);
|
|
1919
|
+
await app.listen(3000);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (cluster.isPrimary) {
|
|
1923
|
+
const numCPUs = os.cpus().length;
|
|
1924
|
+
console.log(`Master process ${process.pid}, forking ${numCPUs} workers`);
|
|
1925
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
1926
|
+
cluster.fork();
|
|
1927
|
+
}
|
|
1928
|
+
cluster.on('exit', (worker) => {
|
|
1929
|
+
console.log(`Worker ${worker.process.pid} died, restarting...`);
|
|
1930
|
+
cluster.fork();
|
|
1931
|
+
});
|
|
1932
|
+
} else {
|
|
1933
|
+
bootstrap();
|
|
1934
|
+
}
|
|
1935
|
+
```
|
|
1936
|
+
|
|
1937
|
+
> **生产建议**: 优先使用 PM2 或 K8s 水平扩展管理多进程,而非手动 cluster。手动 cluster 适合简单部署场景。
|
|
1938
|
+
|
|
1939
|
+
### 5. 其他优化措施
|
|
1940
|
+
|
|
1941
|
+
```typescript
|
|
1942
|
+
// 启用 shutdown hooks(优雅关闭)
|
|
1943
|
+
app.enableShutdownHooks();
|
|
1944
|
+
|
|
1945
|
+
// 设置全局前缀
|
|
1946
|
+
app.setGlobalPrefix('api');
|
|
1947
|
+
|
|
1948
|
+
// CORS
|
|
1949
|
+
app.enableCors({
|
|
1950
|
+
origin: ['https://example.com'],
|
|
1951
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
1952
|
+
credentials: true,
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
// Helmet 安全头
|
|
1956
|
+
import helmet from 'helmet';
|
|
1957
|
+
app.use(helmet());
|
|
1958
|
+
|
|
1959
|
+
// 速率限制
|
|
1960
|
+
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
|
1961
|
+
|
|
1962
|
+
@Module({
|
|
1963
|
+
imports: [
|
|
1964
|
+
ThrottlerModule.forRoot([{
|
|
1965
|
+
ttl: 60000, // 1 分钟
|
|
1966
|
+
limit: 100, // 最多 100 次请求
|
|
1967
|
+
}]),
|
|
1968
|
+
],
|
|
1969
|
+
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
|
|
1970
|
+
})
|
|
1971
|
+
export class AppModule {}
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
---
|
|
1975
|
+
|
|
1976
|
+
## 常见陷阱
|
|
1977
|
+
|
|
1978
|
+
### 1. 循环依赖导致 undefined
|
|
1979
|
+
|
|
1980
|
+
```typescript
|
|
1981
|
+
// ❌ 错误:A 和 B 互相注入,其中一个会是 undefined
|
|
1982
|
+
@Injectable()
|
|
1983
|
+
export class ServiceA {
|
|
1984
|
+
constructor(private serviceB: ServiceB) {} // 可能是 undefined
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// ✅ 解决:使用 forwardRef 或提取公共逻辑
|
|
1988
|
+
@Injectable()
|
|
1989
|
+
export class ServiceA {
|
|
1990
|
+
constructor(
|
|
1991
|
+
@Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB,
|
|
1992
|
+
) {}
|
|
1993
|
+
}
|
|
1994
|
+
```
|
|
1995
|
+
|
|
1996
|
+
### 2. 请求作用域的性能问题
|
|
1997
|
+
|
|
1998
|
+
```typescript
|
|
1999
|
+
// ❌ 不当:对所有服务使用请求作用域
|
|
2000
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
2001
|
+
export class HeavyService {} // 每次请求都创建新实例
|
|
2002
|
+
|
|
2003
|
+
// ✅ 正确:仅在确实需要请求上下文的服务上使用
|
|
2004
|
+
@Injectable()
|
|
2005
|
+
export class HeavyService {
|
|
2006
|
+
doWork(requestContext: RequestContext) {
|
|
2007
|
+
// 通过参数传递请求上下文,保持单例
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
### 3. 忘记导出 Provider
|
|
2013
|
+
|
|
2014
|
+
```typescript
|
|
2015
|
+
// ❌ 错误:其他模块无法注入 UsersService
|
|
2016
|
+
@Module({
|
|
2017
|
+
providers: [UsersService],
|
|
2018
|
+
// 缺少 exports
|
|
2019
|
+
})
|
|
2020
|
+
export class UsersModule {}
|
|
2021
|
+
|
|
2022
|
+
// ✅ 正确
|
|
2023
|
+
@Module({
|
|
2024
|
+
providers: [UsersService],
|
|
2025
|
+
exports: [UsersService],
|
|
2026
|
+
})
|
|
2027
|
+
export class UsersModule {}
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
### 4. 异步初始化陷阱
|
|
2031
|
+
|
|
2032
|
+
```typescript
|
|
2033
|
+
// ❌ 错误:同步 forRoot 中使用异步操作
|
|
2034
|
+
@Module({})
|
|
2035
|
+
export class DatabaseModule {
|
|
2036
|
+
static forRoot() {
|
|
2037
|
+
return {
|
|
2038
|
+
module: DatabaseModule,
|
|
2039
|
+
providers: [{
|
|
2040
|
+
provide: 'DB',
|
|
2041
|
+
useValue: connectToDb(), // Promise 未 await!
|
|
2042
|
+
}],
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// ✅ 正确:使用 useFactory + async
|
|
2048
|
+
@Module({})
|
|
2049
|
+
export class DatabaseModule {
|
|
2050
|
+
static forRoot(): DynamicModule {
|
|
2051
|
+
return {
|
|
2052
|
+
module: DatabaseModule,
|
|
2053
|
+
providers: [{
|
|
2054
|
+
provide: 'DB',
|
|
2055
|
+
useFactory: async () => {
|
|
2056
|
+
const conn = await connectToDb();
|
|
2057
|
+
return conn;
|
|
2058
|
+
},
|
|
2059
|
+
}],
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
```
|
|
2064
|
+
|
|
2065
|
+
### 5. 测试中未正确清理
|
|
2066
|
+
|
|
2067
|
+
```typescript
|
|
2068
|
+
// ❌ 错误:测试间共享状态导致随机失败
|
|
2069
|
+
describe('Service', () => {
|
|
2070
|
+
let app: INestApplication;
|
|
2071
|
+
beforeAll(async () => { app = /* ... */ });
|
|
2072
|
+
// 测试完没有关闭
|
|
2073
|
+
|
|
2074
|
+
// ✅ 正确:每次清理
|
|
2075
|
+
afterAll(async () => {
|
|
2076
|
+
await app.close();
|
|
2077
|
+
});
|
|
2078
|
+
});
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
### 6. ValidationPipe 未配置 whitelist
|
|
2082
|
+
|
|
2083
|
+
```typescript
|
|
2084
|
+
// ❌ 危险:客户端可注入任意属性
|
|
2085
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
2086
|
+
// POST { name: "Alice", isAdmin: true } -> isAdmin 会被传入 DTO
|
|
2087
|
+
|
|
2088
|
+
// ✅ 安全:只保留 DTO 中定义的属性
|
|
2089
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
2090
|
+
whitelist: true,
|
|
2091
|
+
forbidNonWhitelisted: true,
|
|
2092
|
+
}));
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
### 7. 生产环境 synchronize: true
|
|
2096
|
+
|
|
2097
|
+
```typescript
|
|
2098
|
+
// ❌ 灾难性:自动同步会删除数据列
|
|
2099
|
+
TypeOrmModule.forRoot({
|
|
2100
|
+
synchronize: true, // 绝对不能在生产环境开启
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
// ✅ 正确:使用迁移管理数据库变更
|
|
2104
|
+
TypeOrmModule.forRoot({
|
|
2105
|
+
synchronize: false,
|
|
2106
|
+
migrations: ['dist/migrations/*.js'],
|
|
2107
|
+
migrationsRun: true,
|
|
2108
|
+
});
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
---
|
|
2112
|
+
|
|
2113
|
+
## 最佳实践
|
|
2114
|
+
|
|
2115
|
+
1. **模块边界清晰**: 每个功能域一个模块,通过 exports 控制可见性
|
|
2116
|
+
2. **DTO 验证一切输入**: 使用 class-validator + ValidationPipe(whitelist: true)
|
|
2117
|
+
3. **依赖注入优于直接导入**: 提高可测试性,便于替换实现
|
|
2118
|
+
4. **使用 ConfigModule 管理配置**: 不要直接读取 process.env
|
|
2119
|
+
5. **全局异常过滤器**: 统一错误响应格式
|
|
2120
|
+
6. **全局拦截器**: 响应包装、日志、超时控制
|
|
2121
|
+
7. **接口而非实现**: 使用自定义 token 注入,便于切换实现
|
|
2122
|
+
8. **健康检查端点**: 必须暴露 /health 用于 K8s 探针
|
|
2123
|
+
9. **优雅关闭**: 启用 shutdownHooks,处理 SIGTERM
|
|
2124
|
+
10. **日志结构化**: 使用 Pino 或 Winston,输出 JSON 格式日志
|
|
2125
|
+
|
|
2126
|
+
---
|
|
2127
|
+
|
|
2128
|
+
## 参考资料
|
|
2129
|
+
|
|
2130
|
+
- [NestJS 官方文档](https://docs.nestjs.com/)
|
|
2131
|
+
- [NestJS GitHub](https://github.com/nestjs/nest)
|
|
2132
|
+
- [Awesome NestJS](https://github.com/nestjsx/awesome-nestjs)
|
|
2133
|
+
- [NestJS 官方示例集合](https://github.com/nestjs/nest/tree/master/sample)
|
|
2134
|
+
|
|
2135
|
+
---
|
|
2136
|
+
|
|
2137
|
+
## Agent Checklist
|
|
2138
|
+
|
|
2139
|
+
- [ ] 项目使用 `nest new` 或手动创建时,确认 `reflect-metadata` 和 `rxjs` 已安装
|
|
2140
|
+
- [ ] 模块结构遵循功能域划分,每个域独立模块 + 控制器 + 服务
|
|
2141
|
+
- [ ] 全局 ValidationPipe 配置 whitelist + forbidNonWhitelisted + transform
|
|
2142
|
+
- [ ] 全局异常过滤器统一错误响应格式
|
|
2143
|
+
- [ ] 数据库 synchronize 在生产环境为 false,使用迁移管理
|
|
2144
|
+
- [ ] TypeORM 事务使用 QueryRunner 或 DataSource.transaction()
|
|
2145
|
+
- [ ] Prisma 使用 $transaction 保证原子性
|
|
2146
|
+
- [ ] JWT 认证使用 Passport + @nestjs/jwt,token 有过期时间
|
|
2147
|
+
- [ ] RBAC 守卫与 @Roles() 装饰器配合使用
|
|
2148
|
+
- [ ] WebSocket Gateway 配置认证中间件
|
|
2149
|
+
- [ ] 微服务客户端使用 ClientProxy 通信,区分 send(请求-响应)和 emit(事件)
|
|
2150
|
+
- [ ] GraphQL 使用 DataLoader 解决 N+1 查询问题
|
|
2151
|
+
- [ ] 队列任务配置重试策略和失败保留策略
|
|
2152
|
+
- [ ] 缓存使用 Redis,写操作后主动失效相关缓存
|
|
2153
|
+
- [ ] Docker 使用多阶段构建,非 root 用户运行
|
|
2154
|
+
- [ ] K8s 配置 liveness/readiness 探针指向 /health
|
|
2155
|
+
- [ ] 启用 shutdownHooks 实现优雅关闭
|
|
2156
|
+
- [ ] 启用 Helmet + CORS + ThrottlerGuard 安全措施
|
|
2157
|
+
- [ ] 单元测试使用 Testing 模块 Mock 依赖
|
|
2158
|
+
- [ ] E2E 测试使用 overrideProvider/overrideGuard 隔离外部依赖
|
|
2159
|
+
- [ ] 无循环依赖;若无法避免,使用 forwardRef 并注释原因
|
|
2160
|
+
- [ ] 生产环境使用 Fastify 适配器或 PM2/K8s 水平扩展
|
|
2161
|
+
- [ ] ConfigModule 集中管理所有环境变量,不直接使用 process.env
|
|
2162
|
+
|
|
2163
|
+
---
|
|
2164
|
+
|
|
2165
|
+
**文档版本**: v1.0
|
|
2166
|
+
**最后更新**: 2026-03-28
|
|
2167
|
+
**质量评分**: 92/100
|