create-bubbles 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (622) hide show
  1. package/dist/index.js +3 -9
  2. package/package.json +1 -1
  3. package/template-nextjs-vinext-eslint/Dockerfile +4 -4
  4. package/template-nextjs-vinext-eslint/README.md +6 -2
  5. package/template-nextjs-vinext-eslint/docker-compose.yaml +4 -4
  6. package/template-nextjs-vinext-eslint/eslint.config.js +2 -0
  7. package/template-nextjs-vinext-eslint/package.json +28 -26
  8. package/template-nextjs-vinext-eslint/pnpm-workspace.yaml +13 -1
  9. package/template-nextjs-vinext-eslint/src/utils/request/core/index.ts +63 -45
  10. package/template-nextjs-vinext-eslint/src/utils/request/core/utils.ts +12 -6
  11. package/template-nextjs-vinext-eslint/vite.config.ts +1 -1
  12. package/template-react-rsbuild-biome/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  13. package/template-react-rsbuild-biome/AGENTS.md +11 -0
  14. package/template-react-rsbuild-biome/index.html +2 -2
  15. package/template-react-rsbuild-biome/package.json +25 -24
  16. package/template-react-rsbuild-biome/pnpm-workspace.yaml +5 -0
  17. package/template-react-rsbuild-biome/rsbuild.config.ts +1 -1
  18. package/template-react-rsbuild-biome/src/assets/icon/logo.svg +1 -1
  19. package/template-react-rsbuild-biome/src/assets/svg/draft.svg +4 -0
  20. package/template-react-rsbuild-biome/src/components/Icon/svg-icon/README.md +37 -0
  21. package/template-react-rsbuild-biome/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  22. package/template-react-rsbuild-biome/src/env.d.ts +7 -0
  23. package/template-react-rsbuild-biome/src/utils/request/index.ts +3 -4
  24. package/template-react-rsbuild-biome/tsconfig.json +1 -2
  25. package/template-taro-react-oxc/.oxlintrc.json +0 -1
  26. package/template-taro-react-oxc/package.json +53 -52
  27. package/template-taro-react-oxc/pnpm-workspace.yaml +46 -0
  28. package/template-taro-vue-eslint/package.json +56 -56
  29. package/template-vp-monorepo-react-hono/apps/web/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  30. package/template-vp-monorepo-react-hono/apps/web/AGENTS.md +11 -0
  31. package/template-vp-monorepo-react-hono/apps/web/package.json +1 -0
  32. package/template-vp-monorepo-react-hono/apps/web/src/assets/svg/draft.svg +4 -0
  33. package/template-vp-monorepo-react-hono/apps/web/src/components/Icon/svg-icon/README.md +37 -0
  34. package/template-vp-monorepo-react-hono/apps/web/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  35. package/template-vp-monorepo-react-hono/apps/web/tsconfig.json +1 -1
  36. package/template-vp-monorepo-react-hono/apps/web/vite.config.ts +2 -0
  37. package/template-vp-monorepo-react-hono/pnpm-workspace.yaml +1 -0
  38. package/template-vp-monorepo-react-nestjs/.agents/rules//344/273/243/347/240/201/350/256/276/350/256/241.md +4 -0
  39. package/template-vp-monorepo-react-nestjs/.agents/rules//345/205/261/344/272/253/344/273/243/347/240/201.md +4 -0
  40. package/template-vp-monorepo-react-nestjs/.agents/rules//351/234/200/346/261/202/344/270/216/350/256/276/350/256/241/346/226/207/346/241/243.md +50 -0
  41. package/template-vp-monorepo-react-nestjs/.codegraph/.gitignore +5 -0
  42. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/PRD.md +121 -0
  43. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/cleanup-empty-preview.png +0 -0
  44. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/company-created.png +0 -0
  45. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/login-desktop.png +0 -0
  46. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/member-role-assigned.png +0 -0
  47. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/permission-revoked.png +0 -0
  48. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/platform-menu-recovery.png +0 -0
  49. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/project-created.png +0 -0
  50. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/project-member-home.png +0 -0
  51. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/replacement-administrator.png +0 -0
  52. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225/ui/workspace-empty.png +0 -0
  53. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//345/206/263/347/255/226/350/256/260/345/275/225.md +29 -0
  54. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//345/211/215/347/253/257/344/272/244/346/216/245.md +74 -0
  55. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//345/220/216/347/253/257/346/265/213/350/257/225/346/212/245/345/221/212.md +115 -0
  56. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/212/200/346/234/257/345/245/221/347/272/246.md +578 -0
  57. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/265/213/350/257/225/346/212/245/345/221/212.md +137 -0
  58. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/265/213/350/257/225/350/257/201/346/215/256/browser.json +84 -0
  59. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/265/213/350/257/225/350/257/201/346/215/256/http.json +108 -0
  60. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/265/213/350/257/225/350/257/201/346/215/256/legacy-http.json +18 -0
  61. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//346/265/213/350/257/225/350/257/201/346/215/256/legacy.json +24 -0
  62. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//350/201/224/350/260/203/350/256/260/345/275/225.md +76 -0
  63. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//350/277/220/350/241/214/350/257/264/346/230/216.md +117 -0
  64. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//346/235/203/351/231/220/344/270/216/350/217/234/345/215/225//351/234/200/346/261/202/346/226/207/346/241/243.md +275 -0
  65. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//345/206/263/347/255/226/350/256/260/345/275/225.md +35 -0
  66. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//346/212/200/346/234/257/345/245/221/347/272/246.md +744 -0
  67. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//346/265/213/350/257/225/346/212/245/345/221/212.md +174 -0
  68. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//350/201/224/350/260/203/350/256/260/345/275/225.md +137 -0
  69. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//350/256/276/350/256/241/346/226/271/346/241/210.md +215 -0
  70. package/template-vp-monorepo-react-nestjs/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//347/273/204/347/273/207/346/236/266/346/236/204//351/234/200/346/261/202/346/226/207/346/241/243.md +421 -0
  71. package/template-vp-monorepo-react-nestjs/.spaces/02./344/270/212/344/274/240/344/270/216/346/226/207/344/273/266//344/270/212/344/274/240/346/224/271/351/200/240//350/256/276/350/256/241/346/226/271/346/241/210.md +851 -0
  72. package/template-vp-monorepo-react-nestjs/.spaces/02./344/270/212/344/274/240/344/270/216/346/226/207/344/273/266//344/270/212/344/274/240/346/270/205/347/220/206//350/256/276/350/256/241/346/226/271/346/241/210.md +1470 -0
  73. package/template-vp-monorepo-react-nestjs/.spaces/02./344/270/212/344/274/240/344/270/216/346/226/207/344/273/266//345/210/206/347/211/207/344/270/212/344/274/240//345/256/236/346/226/275/346/225/231/347/250/213.md +4507 -0
  74. package/template-vp-monorepo-react-nestjs/.spaces/03./344/273/273/345/212/241/351/230/237/345/210/227//345/205/254/345/205/261/351/230/237/345/210/227//350/256/276/350/256/241/346/226/271/346/241/210.md +1897 -0
  75. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/accounts-mobile-320.png +0 -0
  76. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/accounts-mobile-360.png +0 -0
  77. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/accounts-mobile-390.png +0 -0
  78. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/companies-desktop-data.png +0 -0
  79. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/companies-desktop-empty.png +0 -0
  80. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/companies-mobile-320.png +0 -0
  81. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/companies-mobile-360.png +0 -0
  82. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/companies-mobile-390.png +0 -0
  83. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/company-create-dialog.png +0 -0
  84. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/company-projects-1440.png +0 -0
  85. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/company-projects-390.png +0 -0
  86. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/login-1440.png +0 -0
  87. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/login-390.png +0 -0
  88. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/project-home-1440.png +0 -0
  89. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/project-home-390.png +0 -0
  90. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/register-1440.png +0 -0
  91. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/register-390.png +0 -0
  92. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/workspaces-1440.png +0 -0
  93. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/workspaces-390.png +0 -0
  94. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273/ui/workspaces-empty.png +0 -0
  95. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273//345/267/245/345/205/267/fixtures.mjs +132 -0
  96. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273//345/267/245/345/205/267/visual-check.mjs +274 -0
  97. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273//346/265/213/350/257/225/350/257/201/346/215/256/visual-report.json +104 -0
  98. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//344/270/207/347/211/251/346/242/246/345/271/273//351/252/214/350/257/201/350/257/264/346/230/216.md +25 -0
  99. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//344/272/244/344/273/230/350/256/260/345/275/225.md +12 -0
  100. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//346/265/213/350/257/225/346/212/245/345/221/212.md +19 -0
  101. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//350/256/276/350/256/241/346/226/271/346/241/210.md +18 -0
  102. package/template-vp-monorepo-react-nestjs/.spaces/04./347/225/214/351/235/242/344/270/216/344/270/273/351/242/230//345/233/275/351/231/205/345/214/226//351/234/200/346/261/202/346/226/207/346/241/243.md +24 -0
  103. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//345/271/263/345/217/260/346/223/215/344/275/234/346/211/213/345/206/214.md +397 -0
  104. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273//345/244/207/344/273/275/backup.dump +0 -0
  105. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273//345/244/207/344/273/275/export-dump.ps1 +34 -0
  106. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273//345/244/207/344/273/275/restore-dump.ps1 +73 -0
  107. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273//346/223/215/344/275/234/346/214/207/345/215/227.md +227 -0
  108. package/template-vp-monorepo-react-nestjs/.spaces/05./350/277/220/350/241/214/347/273/264/346/212/244//346/225/260/346/215/256/345/272/223/350/277/201/347/247/273//351/234/200/346/261/202/346/226/207/346/241/243.md +31 -0
  109. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/274/232/350/257/235/347/256/241/347/220/206//346/212/200/346/234/257/345/245/221/347/272/246.md +93 -0
  110. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/274/232/350/257/235/347/256/241/347/220/206//351/234/200/346/261/202/346/226/207/346/241/243.md +87 -0
  111. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201/PRD.md +55 -0
  112. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//346/212/200/346/234/257/345/245/221/347/272/246.md +92 -0
  113. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//346/265/213/350/257/225/346/212/245/345/221/212.md +38 -0
  114. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//350/201/224/350/260/203/350/256/260/345/275/225.md +26 -0
  115. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//350/256/276/350/256/241/346/226/271/346/241/210.md +53 -0
  116. package/template-vp-monorepo-react-nestjs/.spaces/06./350/256/244/350/257/201/344/270/216/344/274/232/350/257/235//344/277/256/346/224/271/345/257/206/347/240/201//351/234/200/346/261/202/346/226/207/346/241/243.md +63 -0
  117. package/template-vp-monorepo-react-nestjs/.spaces/07.API/344/270/216/351/224/231/350/257/257//346/212/200/346/234/257/345/245/221/347/272/246.md +95 -0
  118. package/template-vp-monorepo-react-nestjs/.spaces/README.md +85 -0
  119. package/template-vp-monorepo-react-nestjs/.spaces//346/226/207/346/241/243/350/247/204/350/214/203.md +97 -0
  120. package/template-vp-monorepo-react-nestjs/AGENTS.md +18 -10
  121. package/template-vp-monorepo-react-nestjs/README.md +3 -27
  122. package/template-vp-monorepo-react-nestjs/apps/server/.agents/rules/error-handling.md +84 -0
  123. package/template-vp-monorepo-react-nestjs/apps/server/.env.development +60 -0
  124. package/template-vp-monorepo-react-nestjs/apps/server/.env.production +16 -0
  125. package/template-vp-monorepo-react-nestjs/apps/server/AGENTS.md +9 -0
  126. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0000_military_colonel_america.sql +11 -0
  127. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0001_loud_siren.sql +27 -0
  128. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0002_enterprise_access.sql +182 -0
  129. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0003_cute_killmonger.sql +357 -0
  130. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0004_company-member-invitations.sql +25 -0
  131. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0005_menu_svg_icons.sql +54 -0
  132. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0000_snapshot.json +97 -0
  133. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0001_snapshot.json +354 -0
  134. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0002_snapshot.json +1801 -0
  135. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0003_snapshot.json +3511 -0
  136. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0004_snapshot.json +3713 -0
  137. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/_journal.json +48 -0
  138. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/rollback/0003_organization_backfill.sql +32 -0
  139. package/template-vp-monorepo-react-nestjs/apps/server/package.json +9 -3
  140. package/template-vp-monorepo-react-nestjs/apps/server/src/access-init.ts +45 -0
  141. package/template-vp-monorepo-react-nestjs/apps/server/src/app.controller.ts +3 -0
  142. package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +41 -6
  143. package/template-vp-monorepo-react-nestjs/apps/server/src/app.service.ts +3 -0
  144. package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +17 -0
  145. package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/session.constants.ts +9 -0
  146. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/access-policy.decorator.ts +18 -0
  147. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/current-auth.decorator.ts +4 -0
  148. package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/public.decorator.ts +3 -0
  149. package/template-vp-monorepo-react-nestjs/apps/server/src/common/exceptions/app.exception.ts +9 -0
  150. package/template-vp-monorepo-react-nestjs/apps/server/src/common/filters/global-exception.filter.ts +38 -5
  151. package/template-vp-monorepo-react-nestjs/apps/server/src/common/security/password-security-event.ts +98 -0
  152. package/template-vp-monorepo-react-nestjs/apps/server/src/config/app.config.ts +3 -0
  153. package/template-vp-monorepo-react-nestjs/apps/server/src/config/database.config.ts +3 -0
  154. package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +2 -0
  155. package/template-vp-monorepo-react-nestjs/apps/server/src/config/llm.config.ts +3 -0
  156. package/template-vp-monorepo-react-nestjs/apps/server/src/config/queue.config.ts +119 -0
  157. package/template-vp-monorepo-react-nestjs/apps/server/src/config/redis.config.ts +3 -0
  158. package/template-vp-monorepo-react-nestjs/apps/server/src/config/session.config.ts +7 -0
  159. package/template-vp-monorepo-react-nestjs/apps/server/src/config/storage.config.ts +42 -0
  160. package/template-vp-monorepo-react-nestjs/apps/server/src/database/access.schema.ts +394 -0
  161. package/template-vp-monorepo-react-nestjs/apps/server/src/database/db.module.ts +3 -0
  162. package/template-vp-monorepo-react-nestjs/apps/server/src/database/organization.schema.ts +453 -0
  163. package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +63 -1
  164. package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +5 -0
  165. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access-http.ts +29 -0
  166. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.errors.ts +130 -0
  167. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.guard.ts +63 -0
  168. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.module.ts +27 -0
  169. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.service.ts +441 -0
  170. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.store.ts +68 -0
  171. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/access.validation.ts +44 -0
  172. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/maintenance/permissions-cleanup.controller.ts +27 -0
  173. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/maintenance/permissions-cleanup.service.ts +260 -0
  174. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/maintenance/permissions-cleanup.validation.ts +7 -0
  175. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/roles/roles.controller.ts +80 -0
  176. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/roles/roles.service.ts +249 -0
  177. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/roles/roles.validation.ts +24 -0
  178. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/seed/access-seed.service.ts +183 -0
  179. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/workspaces/workspaces.controller.ts +28 -0
  180. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/workspaces/workspaces.service.ts +94 -0
  181. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/access/workspaces/workspaces.validation.ts +5 -0
  182. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/audit/audit.controller.ts +26 -0
  183. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/audit/audit.module.ts +11 -0
  184. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/audit/audit.service.ts +74 -0
  185. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/audit/audit.validation.ts +11 -0
  186. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.controller.ts +25 -0
  187. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.errors.ts +10 -0
  188. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.module.ts +1 -1
  189. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.repository.ts +60 -0
  190. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +69 -13
  191. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +2 -2
  192. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/register.dto.ts +1 -1
  193. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/guards/session-auth.guard.ts +8 -0
  194. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/password/change-password.dto.ts +17 -0
  195. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/password.service.ts +6 -0
  196. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-store.service.ts +46 -0
  197. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-token.service.ts +12 -0
  198. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.script.ts +7 -1
  199. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/companies/companies.controller.ts +105 -0
  200. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/companies/companies.module.ts +12 -0
  201. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/companies/companies.service.ts +523 -0
  202. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/accounts/accounts.controller.ts +72 -0
  203. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/accounts/accounts.service.ts +154 -0
  204. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/accounts/accounts.validation.ts +8 -0
  205. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/accounts/password-reset/password-reset.service.ts +81 -0
  206. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/accounts/password-reset/reset-password.validation.ts +13 -0
  207. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/administrators/administrators.service.ts +116 -0
  208. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/administrators/administrators.validation.ts +1 -0
  209. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/invitations/company-member-invitations.controller.ts +69 -0
  210. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/invitations/member-invitation-acceptance.controller.ts +23 -0
  211. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/invitations/member-invitations.service.ts +344 -0
  212. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/invitations/member-invitations.validation.ts +23 -0
  213. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/members.controller.ts +88 -0
  214. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/members.module.ts +31 -0
  215. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/members.service.ts +573 -0
  216. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/members/members.validation.ts +19 -0
  217. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/menus/menus.controller.ts +56 -0
  218. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/menus/menus.module.ts +11 -0
  219. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/menus/menus.service.ts +230 -0
  220. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/menus/menus.validation.ts +30 -0
  221. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/candidates/global-account/global-account-candidates.controller.ts +35 -0
  222. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/candidates/global-account/global-account-candidates.service.ts +109 -0
  223. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/candidates/member-candidates.controller.ts +60 -0
  224. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/candidates/member-candidates.service.ts +939 -0
  225. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/initialization/project-organization-initialization.service.ts +189 -0
  226. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/organization.module.ts +34 -0
  227. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/organization.store.ts +150 -0
  228. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/organization.validation.ts +23 -0
  229. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/positions/positions.controller.ts +160 -0
  230. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/positions/positions.service.ts +636 -0
  231. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/templates/organization-templates.controller.ts +77 -0
  232. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/templates/organization-templates.service.ts +501 -0
  233. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/templates/organization-templates.store.ts +163 -0
  234. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/units/organization-units.controller.ts +187 -0
  235. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/organization/units/organization-units.service.ts +947 -0
  236. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/projects/projects.controller.ts +94 -0
  237. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/projects/projects.module.ts +13 -0
  238. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/projects/projects.service.ts +369 -0
  239. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/queue-worker.bootstrap.ts +27 -0
  240. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.constants.ts +24 -0
  241. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.contracts.ts +39 -0
  242. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.module.ts +61 -0
  243. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.processor.ts +75 -0
  244. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.service.ts +67 -0
  245. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/db.controller.ts +9 -0
  246. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/db.service.ts +9 -0
  247. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/error-catelog.spec.ts +34 -0
  248. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/redis/redis.controller.ts +9 -0
  249. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/redis/redis.service.ts +18 -0
  250. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/initiate-mutipart-upload.dto.ts +26 -0
  251. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/upload-params.dto.ts +15 -0
  252. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/exact-size.transform.ts +77 -0
  253. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/minio-storage.adapter.ts +276 -0
  254. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/storage.port.ts +82 -0
  255. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.constants.ts +32 -0
  256. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.controller.ts +134 -0
  257. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.errors.ts +80 -0
  258. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.module.ts +20 -0
  259. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.reponsitory.ts +193 -0
  260. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.service.ts +774 -0
  261. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/utils/object-key.ts +14 -0
  262. package/template-vp-monorepo-react-nestjs/apps/server/src/utils/server-address.ts +6 -0
  263. package/template-vp-monorepo-react-nestjs/apps/server/test/access/access-policy.spec.ts +95 -0
  264. package/template-vp-monorepo-react-nestjs/apps/server/test/access/access.integration.spec.ts +2250 -0
  265. package/template-vp-monorepo-react-nestjs/apps/server/test/access/member-onboarding.validation.spec.ts +78 -0
  266. package/template-vp-monorepo-react-nestjs/apps/server/test/auth/password-operations.spec.ts +433 -0
  267. package/template-vp-monorepo-react-nestjs/apps/server/test/auth/password.integration.spec.ts +300 -0
  268. package/template-vp-monorepo-react-nestjs/apps/server/test/error-catalog.spec.ts +6 -1
  269. package/template-vp-monorepo-react-nestjs/apps/server/test/menus.validation.spec.ts +28 -0
  270. package/template-vp-monorepo-react-nestjs/apps/server/test/organization/organization-domain.spec.ts +260 -0
  271. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.integration.spec.ts +120 -0
  272. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.processor.spec.ts +63 -0
  273. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.service.spec.ts +106 -0
  274. package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.scripts.json +5 -0
  275. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  276. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//345/274/271/347/252/227/347/273/204/344/273/266.md +4 -0
  277. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//346/226/207/344/273/266/346/213/206/345/210/206.md +4 -0
  278. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//347/273/204/344/273/266/345/221/275/345/220/215.md +3 -0
  279. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//347/273/204/344/273/266/345/244/215/347/224/250.md +7 -0
  280. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//350/241/250/345/215/225/345/233/236/345/241/253.md +4 -0
  281. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//350/241/250/346/240/274/351/241/265/351/235/242.md +11 -0
  282. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules//351/241/265/351/235/242/346/216/245/345/217/243.md +3 -0
  283. package/template-vp-monorepo-react-nestjs/apps/web/.env.dev +1 -1
  284. package/template-vp-monorepo-react-nestjs/apps/web/AGENTS.md +25 -0
  285. package/template-vp-monorepo-react-nestjs/apps/web/README.MD +181 -0
  286. package/template-vp-monorepo-react-nestjs/apps/web/index.html +3 -2
  287. package/template-vp-monorepo-react-nestjs/apps/web/package.json +7 -1
  288. package/template-vp-monorepo-react-nestjs/apps/web/public/favicon.svg +9 -1
  289. package/template-vp-monorepo-react-nestjs/apps/web/src/App.module.css +5 -0
  290. package/template-vp-monorepo-react-nestjs/apps/web/src/App.tsx +30 -12
  291. package/template-vp-monorepo-react-nestjs/apps/web/src/api/auth.ts +22 -0
  292. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/accounts.svg +4 -0
  293. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/audit-log.svg +4 -0
  294. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/companies.svg +5 -0
  295. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/dashboard.svg +6 -0
  296. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/draft.svg +4 -0
  297. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/home.svg +4 -0
  298. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/members.svg +4 -0
  299. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/menu-item.svg +4 -0
  300. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/menus.svg +6 -0
  301. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/organization-template.svg +4 -0
  302. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/organization.svg +6 -0
  303. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/positions.svg +4 -0
  304. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/profile.svg +5 -0
  305. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/projects.svg +4 -0
  306. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/roles.svg +4 -0
  307. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Brand/Brand.module.css +125 -0
  308. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Brand/Brand.tsx +56 -0
  309. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.module.css +78 -0
  310. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.tsx +105 -0
  311. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/README.md +34 -0
  312. package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.module.css +54 -0
  313. package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.tsx +41 -0
  314. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/SvgAsset/IconSelector.tsx +38 -0
  315. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/SvgAsset/SvgAssetIcon.tsx +14 -0
  316. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/SvgAsset/SvgAssetRegistry.ts +32 -0
  317. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/SvgAsset/index.ts +10 -0
  318. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/README.md +37 -0
  319. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/SvgIcon.tsx +25 -0
  320. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.module.css +7 -0
  321. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.tsx +6 -1
  322. package/template-vp-monorepo-react-nestjs/apps/web/src/components/LocaleSwitch/LocaleSwitch.tsx +52 -0
  323. package/template-vp-monorepo-react-nestjs/apps/web/src/components/PasswordActions/ChangePasswordDialog.tsx +90 -0
  324. package/template-vp-monorepo-react-nestjs/apps/web/src/components/PasswordActions/PasswordFields.module.css +12 -0
  325. package/template-vp-monorepo-react-nestjs/apps/web/src/components/PasswordActions/PasswordFields.tsx +54 -0
  326. package/template-vp-monorepo-react-nestjs/apps/web/src/components/PasswordActions/ResetAccountPasswordDialog.tsx +100 -0
  327. package/template-vp-monorepo-react-nestjs/apps/web/src/components/PasswordActions/change-password-session.ts +13 -0
  328. package/template-vp-monorepo-react-nestjs/apps/web/src/components/RouteTransition/RouteTransition.css +60 -0
  329. package/template-vp-monorepo-react-nestjs/apps/web/src/components/RouteTransition/RouteTransition.tsx +44 -0
  330. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/BasicTableSelector/BasicTableSelector.tsx +218 -0
  331. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/BasicTableSelector/BasicTableSelectorSelection.ts +91 -0
  332. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/BasicTableSelector/BasicTableSelectorTypes.ts +60 -0
  333. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/BasicTableSelector/README.md +110 -0
  334. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/BasicTableSelector/index.ts +7 -0
  335. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/GlobalAccountSelect/GlobalAccountSelect.tsx +211 -0
  336. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/GlobalAccountSelect/GlobalAccountSelectTypes.ts +75 -0
  337. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/GlobalAccountSelect/README.md +35 -0
  338. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/GlobalAccountSelect/index.ts +9 -0
  339. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/OrganizationMemberBrowser.tsx +146 -0
  340. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/OrganizationMemberCandidateColumns.tsx +137 -0
  341. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/OrganizationMemberFilterPanel.tsx +209 -0
  342. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/OrganizationMemberSelector.tsx +196 -0
  343. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/OrganizationMemberSelectorTypes.ts +103 -0
  344. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/README.md +8 -0
  345. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/OrganizationMemberSelector/index.ts +13 -0
  346. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/README.md +13 -0
  347. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Selector/index.ts +30 -0
  348. package/template-vp-monorepo-react-nestjs/apps/web/src/config/theme.ts +52 -0
  349. package/template-vp-monorepo-react-nestjs/apps/web/src/hooks/useLatestDialogRequest.ts +71 -0
  350. package/template-vp-monorepo-react-nestjs/apps/web/src/i18n/config.ts +40 -0
  351. package/template-vp-monorepo-react-nestjs/apps/web/src/i18n/index.ts +10 -0
  352. package/template-vp-monorepo-react-nestjs/apps/web/src/i18n/instance.ts +36 -0
  353. package/template-vp-monorepo-react-nestjs/apps/web/src/i18n/ui-locale.ts +30 -0
  354. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/BasicLayout/index.tsx +113 -0
  355. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/WorkspaceLayout/components/WorkspaceNavigationBoundary.tsx +50 -0
  356. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/WorkspaceLayout/index.tsx +196 -0
  357. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/WorkspaceLayout/workspace-landing.css +119 -0
  358. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/WorkspaceLayout/workspace-surfaces.css +110 -0
  359. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/WorkspaceLayout/workspace.css +223 -0
  360. package/template-vp-monorepo-react-nestjs/apps/web/src/locales/en_US.json +639 -1
  361. package/template-vp-monorepo-react-nestjs/apps/web/src/locales/zh_CN.json +621 -1
  362. package/template-vp-monorepo-react-nestjs/apps/web/src/main.tsx +8 -2
  363. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/api.ts +226 -0
  364. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/audit.tsx +169 -0
  365. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/AdministratorDialog.tsx +128 -0
  366. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/CleanupDialog.tsx +140 -0
  367. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/CompanyHierarchyDialog.tsx +244 -0
  368. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/EntityFormDialog.tsx +285 -0
  369. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/EntitySelectors/index.ts +6 -0
  370. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/EntitySelectors/selector-requests.ts +59 -0
  371. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberAffiliations/MemberOrganizationsDialog.tsx +188 -0
  372. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberAffiliations/MemberPositionsDialog.tsx +116 -0
  373. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberAffiliations/affiliation-loaders.ts +18 -0
  374. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberAffiliations/index.ts +5 -0
  375. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/AddProjectMemberDialog.tsx +75 -0
  376. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/CompanyMemberInvitation/CompanyMemberInvitationDialog.tsx +246 -0
  377. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/CompanyMemberInvitation/InvitationRecordsTable.tsx +152 -0
  378. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/CompanyMemberInvitation/invitation-link.ts +9 -0
  379. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/MemberOnboardingActions.tsx +79 -0
  380. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/RegisterCompanyMember/RegisterCompanyMemberDialog.module.css +12 -0
  381. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberOnboarding/RegisterCompanyMember/RegisterCompanyMemberDialog.tsx +122 -0
  382. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberRolesDialog.tsx +80 -0
  383. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberTable/MemberTableColumns.tsx +234 -0
  384. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberTable/index.ts +2 -0
  385. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MemberTable/member-candidate-cache.ts +12 -0
  386. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/MenuFormDialog.tsx +251 -0
  387. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/RoleFormDialog.tsx +73 -0
  388. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/components/RolePermissionsDialog.tsx +185 -0
  389. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/entities.tsx +268 -0
  390. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/home.tsx +67 -0
  391. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/members.tsx +310 -0
  392. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/menus/api.ts +74 -0
  393. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/menus/index.tsx +325 -0
  394. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/profile.tsx +160 -0
  395. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/roles.tsx +173 -0
  396. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/access/use-access.ts +35 -0
  397. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/error/404.tsx +20 -0
  398. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/error/error.tsx +42 -0
  399. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.module.css +23 -0
  400. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.tsx +35 -0
  401. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/components/ProjectFormDialog.tsx +137 -0
  402. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/columns.tsx +126 -0
  403. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/index.ts +87 -0
  404. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/components/DraftProjectFormDialog.tsx +147 -0
  405. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/columns.tsx +134 -0
  406. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/index.ts +39 -0
  407. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/projects.ts +56 -0
  408. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.module.css +49 -0
  409. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.tsx +183 -0
  410. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.module.css +53 -0
  411. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.tsx +150 -0
  412. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.module.css +23 -0
  413. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.tsx +28 -42
  414. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/api.ts +14 -0
  415. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/index.tsx +163 -0
  416. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/login-form.css +199 -0
  417. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/login.css +186 -0
  418. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/member-invitations/accept.css +47 -0
  419. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/member-invitations/accept.tsx +143 -0
  420. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/member-invitations/api.ts +20 -0
  421. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/api.ts +91 -0
  422. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/MoveOrganizationUnitDialog.tsx +82 -0
  423. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/OrganizationMemberColumns.tsx +87 -0
  424. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/OrganizationMemberHeader.tsx +102 -0
  425. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/OrganizationTreeModel.tsx +59 -0
  426. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/OrganizationTreePanel.tsx +88 -0
  427. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/index.ts +11 -0
  428. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/organization-loaders.ts +47 -0
  429. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationBrowser/organization-member-filter-loaders.ts +187 -0
  430. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationMembersDialog.tsx +207 -0
  431. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/components/OrganizationUnitDialog.tsx +110 -0
  432. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization/index.tsx +258 -0
  433. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization-templates/api.ts +53 -0
  434. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization-templates/components/OrganizationTemplateDialog/TemplateDefinitionTabs.tsx +189 -0
  435. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization-templates/components/OrganizationTemplateDialog/index.tsx +135 -0
  436. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization-templates/components/OrganizationTemplateDialog/types.ts +25 -0
  437. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/organization-templates/index.tsx +201 -0
  438. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/positions/api.ts +81 -0
  439. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/positions/components/PositionFormDialog.tsx +86 -0
  440. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/positions/components/PositionMembersDialog.tsx +102 -0
  441. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/positions/index.tsx +198 -0
  442. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/positions/loaders.ts +35 -0
  443. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/register/api.ts +14 -0
  444. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/register/index.tsx +157 -0
  445. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/workspaces/api.ts +32 -0
  446. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/workspaces/index.tsx +146 -0
  447. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/workspaces/state.ts +24 -0
  448. package/template-vp-monorepo-react-nestjs/apps/web/src/router/AGENTS.MD +6 -0
  449. package/template-vp-monorepo-react-nestjs/apps/web/src/router/index.tsx +3 -23
  450. package/template-vp-monorepo-react-nestjs/apps/web/src/router/lazy-load/index.tsx +26 -0
  451. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/README.md +23 -0
  452. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/auth.ts +6 -0
  453. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/index.ts +4 -0
  454. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/workspace/access.ts +23 -0
  455. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/workspace/data.ts +55 -0
  456. package/template-vp-monorepo-react-nestjs/apps/web/src/router/middleware/workspace/entry.ts +52 -0
  457. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/access/page.tsx +20 -0
  458. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/auth.tsx +17 -0
  459. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/company.tsx +30 -0
  460. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/entry.tsx +20 -0
  461. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/examples.tsx +23 -0
  462. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/fallback.tsx +13 -0
  463. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/index.tsx +32 -0
  464. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/platform.tsx +27 -0
  465. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/project.tsx +28 -0
  466. package/template-vp-monorepo-react-nestjs/apps/web/src/router/page-registry.tsx +94 -0
  467. package/template-vp-monorepo-react-nestjs/apps/web/src/styles/dream.css +34 -0
  468. package/template-vp-monorepo-react-nestjs/apps/web/src/styles/index.css +17 -4
  469. package/template-vp-monorepo-react-nestjs/apps/web/src/types/auto-imports.d.ts +26 -0
  470. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/organization-assignment.ts +66 -0
  471. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/core/index.ts +101 -58
  472. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/core/utils.ts +5 -0
  473. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/index.ts +16 -6
  474. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/workspace.ts +82 -0
  475. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/safe-next.ts +36 -0
  476. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/cookie.ts +51 -0
  477. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/session.ts +34 -0
  478. package/template-vp-monorepo-react-nestjs/apps/web/test/draft-projects.spec.ts +89 -0
  479. package/template-vp-monorepo-react-nestjs/apps/web/test/i18n-locale.spec.ts +22 -0
  480. package/template-vp-monorepo-react-nestjs/apps/web/test/icon-selector.spec.tsx +23 -0
  481. package/template-vp-monorepo-react-nestjs/apps/web/test/latest-dialog-request.spec.ts +89 -0
  482. package/template-vp-monorepo-react-nestjs/apps/web/test/member-invitation-navigation.spec.ts +34 -0
  483. package/template-vp-monorepo-react-nestjs/apps/web/test/organization-assignment.spec.ts +137 -0
  484. package/template-vp-monorepo-react-nestjs/apps/web/test/password-actions.spec.ts +40 -0
  485. package/template-vp-monorepo-react-nestjs/apps/web/test/password-reset-action.spec.ts +64 -0
  486. package/template-vp-monorepo-react-nestjs/apps/web/test/request.spec.ts +146 -0
  487. package/template-vp-monorepo-react-nestjs/apps/web/test/router/access.spec.ts +136 -0
  488. package/template-vp-monorepo-react-nestjs/apps/web/test/router/config.ts +140 -0
  489. package/template-vp-monorepo-react-nestjs/apps/web/test/router/entry.spec.ts +179 -0
  490. package/template-vp-monorepo-react-nestjs/apps/web/test/router/workspace-data.spec.ts +112 -0
  491. package/template-vp-monorepo-react-nestjs/apps/web/test/selector/global-account-select.spec.ts +311 -0
  492. package/template-vp-monorepo-react-nestjs/apps/web/test/selector/organization-member-selector.spec.ts +454 -0
  493. package/template-vp-monorepo-react-nestjs/apps/web/test/selector/selector-selection.spec.ts +132 -0
  494. package/template-vp-monorepo-react-nestjs/apps/web/test/storage.spec.ts +109 -0
  495. package/template-vp-monorepo-react-nestjs/apps/web/test/tsconfig.json +7 -0
  496. package/template-vp-monorepo-react-nestjs/apps/web/test/workspace-navigation-boundary.spec.tsx +169 -0
  497. package/template-vp-monorepo-react-nestjs/apps/web/test/workspace-navigation.spec.ts +105 -0
  498. package/template-vp-monorepo-react-nestjs/apps/web/test/workspace-requests.spec.ts +174 -0
  499. package/template-vp-monorepo-react-nestjs/apps/web/test/workspaces-password-entry.spec.tsx +71 -0
  500. package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +1 -1
  501. package/template-vp-monorepo-react-nestjs/apps/web/vite.config.ts +85 -29
  502. package/template-vp-monorepo-react-nestjs/apps/web-vue/package.json +2 -0
  503. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/api/index.ts +1 -0
  504. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/useEcharts.ts +13 -1
  505. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/layout/default/header/index.vue +1 -0
  506. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/layout/default/index.vue +6 -2
  507. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/index.ts +1 -0
  508. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/permissionGuard.ts +1 -0
  509. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/index.ts +1 -0
  510. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/index.ts +1 -0
  511. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/modules/user.ts +1 -0
  512. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/types/auto-import.d.ts +2 -0
  513. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/index.ts +30 -0
  514. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/utils.ts +5 -0
  515. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/index.ts +2 -0
  516. package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/config.ts +1 -0
  517. package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.tsbuildinfo +1 -0
  518. package/template-vp-monorepo-react-nestjs/docker-compose.yml +19 -0
  519. package/template-vp-monorepo-react-nestjs/mise.toml +1 -1
  520. package/template-vp-monorepo-react-nestjs/package.json +1 -13
  521. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/command.ts +22 -2
  522. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/config.ts +1 -0
  523. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/files.ts +1 -0
  524. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/load-config.ts +8 -0
  525. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/report.ts +2 -0
  526. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.ts +11 -0
  527. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/sync.ts +12 -0
  528. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/command.test.ts +1 -0
  529. package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/temporary-directory.ts +5 -0
  530. package/template-vp-monorepo-react-nestjs/packages/i18n/core/src/format.ts +1 -0
  531. package/template-vp-monorepo-react-nestjs/packages/i18n/core/src/i18n.ts +14 -0
  532. package/template-vp-monorepo-react-nestjs/packages/i18n/core/src/storage.ts +8 -0
  533. package/template-vp-monorepo-react-nestjs/packages/i18n/core/src/store.ts +9 -0
  534. package/template-vp-monorepo-react-nestjs/packages/i18n/react/src/index.tsx +5 -0
  535. package/template-vp-monorepo-react-nestjs/packages/i18n/react/src/use-store.ts +3 -0
  536. package/template-vp-monorepo-react-nestjs/packages/i18n/vue/src/index.ts +6 -0
  537. package/template-vp-monorepo-react-nestjs/packages/i18n/vue/src/use-store.ts +3 -0
  538. package/template-vp-monorepo-react-nestjs/packages/shared/package.json +5 -1
  539. package/template-vp-monorepo-react-nestjs/packages/shared/src/index.ts +2 -13
  540. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/access-maintenance.ts +68 -0
  541. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/access-menus.ts +94 -0
  542. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/access-resources.ts +168 -0
  543. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/access.ts +71 -0
  544. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/auth.ts +23 -0
  545. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/index.ts +9 -0
  546. package/template-vp-monorepo-react-nestjs/packages/shared/src/types/organization/models.ts +361 -0
  547. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/access-catalog.ts +304 -0
  548. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/access-scope.ts +97 -0
  549. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/account.ts +1 -0
  550. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/index.ts +65 -0
  551. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/organization/contracts.ts +237 -0
  552. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/record.ts +10 -0
  553. package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/session-terminal.ts +1 -0
  554. package/template-vp-monorepo-react-nestjs/packages/shared/test/access.spec.ts +194 -0
  555. package/template-vp-monorepo-react-nestjs/packages/shared/test/organization.spec.ts +168 -0
  556. package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +110 -68
  557. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/browser.mjs +696 -0
  558. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/http-support.mjs +105 -0
  559. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/http.mjs +1091 -0
  560. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/legacy-http.mjs +140 -0
  561. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/legacy.mjs +196 -0
  562. package/template-vp-monorepo-react-nestjs/scripts/enterprise-qa/runtime.mjs +110 -0
  563. package/template-vp-monorepo-react-nestjs/scripts/with-enterprise-test-env.mjs +80 -0
  564. package/template-vp-monorepo-react-nestjs/src/types/auto-imports.d.ts +60 -0
  565. package/template-vp-react/.vscode/settings.json +8 -1
  566. package/template-vp-react/commitlint.config.js +0 -0
  567. package/template-vp-react/package.json +21 -26
  568. package/template-vp-react/pnpm-workspace.yaml +6 -0
  569. package/template-vp-react/src/assets/svg/draft.svg +4 -0
  570. package/template-vp-react/src/components/Icon/svg-icon/README.md +37 -0
  571. package/template-vp-react/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  572. package/template-vp-react/src/types/auto-imports.d.ts +2 -0
  573. package/template-vp-react/src/utils/request/index.ts +18 -5
  574. package/template-vp-react/tsconfig.json +2 -3
  575. package/template-vp-react/vite.config.ts +2 -0
  576. package/template-vp-react-shadcn/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  577. package/template-vp-react-shadcn/.vscode/settings.json +2 -2
  578. package/template-vp-react-shadcn/{agents.md → AGENTS.md} +12 -2
  579. package/template-vp-react-shadcn/README.md +41 -0
  580. package/template-vp-react-shadcn/commitlint.config.js +1 -1
  581. package/template-vp-react-shadcn/components.json +1 -1
  582. package/template-vp-react-shadcn/package.json +25 -32
  583. package/template-vp-react-shadcn/pnpm-workspace.yaml +10 -0
  584. package/template-vp-react-shadcn/src/App.tsx +10 -10
  585. package/template-vp-react-shadcn/src/assets/svg/draft.svg +4 -0
  586. package/template-vp-react-shadcn/src/components/Icon/svg-icon/README.md +37 -0
  587. package/template-vp-react-shadcn/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  588. package/template-vp-react-shadcn/src/components/Loading/PageLoading.tsx +4 -4
  589. package/template-vp-react-shadcn/src/components/Player/VideoJS/index.tsx +94 -78
  590. package/template-vp-react-shadcn/src/components/Player/index.ts +1 -1
  591. package/template-vp-react-shadcn/src/components/ui/button.tsx +25 -32
  592. package/template-vp-react-shadcn/src/components/ui/spinner.tsx +8 -7
  593. package/template-vp-react-shadcn/src/components/ui/toast.tsx +229 -0
  594. package/template-vp-react-shadcn/src/lib/utils.ts +3 -3
  595. package/template-vp-react-shadcn/src/main.tsx +5 -4
  596. package/template-vp-react-shadcn/src/pages/home/index.tsx +43 -35
  597. package/template-vp-react-shadcn/src/router/index.tsx +15 -15
  598. package/template-vp-react-shadcn/src/styles/index.css +4 -4
  599. package/template-vp-react-shadcn/src/styles/shadcn.css +2 -2
  600. package/template-vp-react-shadcn/src/utils/env/index.tsx +2 -2
  601. package/template-vp-react-shadcn/src/utils/request/alova-core/index.ts +102 -30
  602. package/template-vp-react-shadcn/src/utils/request/alova-core/utils.ts +5 -2
  603. package/template-vp-react-shadcn/src/utils/request/index.ts +32 -19
  604. package/template-vp-react-shadcn/src/utils/request/readme.md +41 -38
  605. package/template-vp-react-shadcn/tsconfig.json +2 -3
  606. package/template-vp-react-shadcn/vite.config.ts +29 -15
  607. package/template-vp-vue-eslint-vapor/package.json +29 -36
  608. package/template-vp-vue-eslint-vapor/pnpm-workspace.yaml +29 -1
  609. package/template-vue-vp-eslint/README.md +3 -10
  610. package/template-vue-vp-eslint/package.json +27 -34
  611. package/template-vue-vp-eslint/pnpm-workspace.yaml +27 -2
  612. package/template-vue-vp-eslint/src/layout/default/index.vue +4 -3
  613. package/template-vp-monorepo-react-nestjs/.agents/auth.config.ts +0 -0
  614. package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +0 -991
  615. package/template-vp-monorepo-react-nestjs/apps/server/README.md +0 -3
  616. package/template-vp-monorepo-react-nestjs/apps/web/src/router/modules/.gitkeep +0 -0
  617. package/template-vp-react-shadcn/.oxfmtrc.jsonc +0 -8
  618. package/template-vp-react-shadcn/.oxlintrc.jsonc +0 -3
  619. package/template-vp-react-shadcn/src/components/ui/sonner.tsx +0 -43
  620. /package/template-vp-monorepo-react-nestjs/apps/web/src/assets/{hero.png → image/hero.png} +0 -0
  621. /package/template-vp-monorepo-react-nestjs/apps/web/src/assets/{react.svg → svg/react.svg} +0 -0
  622. /package/template-vp-monorepo-react-nestjs/apps/web/src/assets/{vite.svg → svg/vite.svg} +0 -0
@@ -0,0 +1,1897 @@
1
+ # NestJS BullMQ 公共任务队列接入方案
2
+
3
+ > 状态:历史实施方案;公共队列基础能力已进入当前源码,本文保留设计依据和接入说明,不作为当前验收报告
4
+ > 基线日期:2026-09-03
5
+ > 当前事实复核:2026-09-17
6
+ > 适用范围:apps/server、Redis、BullMQ
7
+ > 面向读者:以前端开发为主、刚开始搭建 NestJS 后端的开发者
8
+ > 本文目标:先建立一套所有后端业务都能复用的公共队列能力,不实现上传清理或其他真实业务任务。
9
+
10
+ 当前源码已经包含 BullMQ 依赖、队列配置、`TaskQueueModule`、`TaskQueueService`、`TaskQueueProcessor`、Worker 启停注册器及相关测试文件。上传清理仍是独立设计,不能因公共队列已落地而视为已经实现。
11
+
12
+ ## 0. 结论先行
13
+
14
+ 本项目第一版公共队列采用:
15
+
16
+ - BullMQ
17
+ - @nestjs/bullmq
18
+ - 当前已有的 Redis
19
+ - 一个默认物理队列 server-tasks
20
+ - 一个公共投递入口 TaskQueueService
21
+ - 一个公共 Worker:TaskQueueProcessor
22
+ - 使用 job.name 区分任务类型,例如 system.queue-smoke.v1、email.send.v1
23
+
24
+ 最终调用关系如下:
25
+
26
+ ```text
27
+ AuthModule / UploadModule / 未来业务模块
28
+ │
29
+ │ 注入 TaskQueueService
30
+ │ enqueue(任务名, 小型 Payload)
31
+ ▼
32
+ BullMQ: server-tasks
33
+ │
34
+ Redis DB 1
35
+ │
36
+ ▼
37
+ TaskQueueProcessor
38
+ │
39
+ 根据 job.name 分发
40
+ ▼
41
+ 具体任务处理逻辑
42
+ ```
43
+
44
+ 这一版先把“公共道路”修好,暂时不往道路上放真实业务车辆。唯一允许加入的是无业务含义的冒烟任务 system.queue-smoke.v1,用来证明:
45
+
46
+ ```text
47
+ 成功投递
48
+ → Redis 中出现 Job
49
+ → Worker 收到 Job
50
+ → Job 执行完成
51
+ ```
52
+
53
+ ### 0.1 为什么选 BullMQ
54
+
55
+ | 方案 | 优点 | 当前不选或选择的原因 |
56
+ | --------------------- | --------------------------------------------- | -------------------------------------------------------------- |
57
+ | Node.js 内存队列 | 最简单 | 服务重启就丢任务,多实例也无法共享,不适合作为后端公共能力 |
58
+ | PostgreSQL 轮询任务表 | 可与业务数据靠得更近 | 需要自己实现抢占、锁、重试、延时和清理;以后做 Outbox 时再引入 |
59
+ | RabbitMQ | 成熟、功能完整 | 当前项目规模下运维和学习成本偏高 |
60
+ | Kafka | 吞吐高,适合事件流 | 不适合当前普通后台任务场景,明显过重 |
61
+ | BullMQ + Redis | NestJS 接入自然,支持重试、延时、并发和持久化 | 适合当前项目,采用 |
62
+
63
+ 不要安装旧的 @nestjs/bull。本方案统一使用 @nestjs/bullmq。
64
+
65
+ 截至本文基线日期,建议版本为:
66
+
67
+ ```yaml
68
+ '@nestjs/bullmq': ^12.0.0
69
+ bullmq: ^6.3.4
70
+ ```
71
+
72
+ 这组版本兼容当前项目的 NestJS 11。
73
+
74
+ ### 0.2 第一版为什么只建一个物理队列
75
+
76
+ server-tasks 适合邮件、通知、轻量文件处理、普通第三方接口调用等任务。这样配置少、理解成本低,足够支撑项目初期。
77
+
78
+ 以下任务以后应拆成独立物理队列和独立 Worker:
79
+
80
+ - AI 推理
81
+ - 视频转码
82
+ - CPU 密集计算
83
+ - 超大文件处理
84
+ - 需要完全不同并发量或超时策略的任务
85
+ - 需要独立扩容、独立发布或独立故障隔离的任务
86
+
87
+ 即使以后拆队列,业务模块仍然应从公共 TaskQueueService 进入,不要各自直接连接 Redis。
88
+
89
+ ### 0.3 搭完后,后端项目是不是基本成型
90
+
91
+ 可以说“后端基础设施骨架基本成型”,但不能说“已经可以直接生产上线”。
92
+
93
+ 搭完本方案后,项目已经具备:
94
+
95
+ - NestJS 应用骨架
96
+ - PostgreSQL 数据访问
97
+ - Redis 与 Session
98
+ - 认证基础
99
+ - 对象存储与上传基础
100
+ - 可复用的异步任务队列
101
+ - 基础错误处理、配置加载、测试和构建流程
102
+
103
+ 仍然需要随着业务继续完善:
104
+
105
+ - 真实业务模块和权限模型
106
+ - 队列任务的幂等实现
107
+ - 日志、指标和告警
108
+ - 数据库与 Redis 备份恢复
109
+ - 限流、安全加固和密钥管理
110
+ - CI/CD、灰度发布和回滚
111
+ - 强一致投递场景所需的 Transactional Outbox
112
+
113
+ 所以准确结论是:骨架成型,业务和生产保障还要继续补。
114
+
115
+ ---
116
+
117
+ ## 1. 前端开发者先理解五个概念
118
+
119
+ 可以把队列理解成餐厅取号。
120
+
121
+ | 队列概念 | 餐厅类比 | 在本项目中的角色 |
122
+ | -------- | -------------------- | ------------------ |
123
+ | Queue | 等候区 | server-tasks |
124
+ | Job | 一张号码单 | 一次具体任务 |
125
+ | Producer | 发号机 | TaskQueueService |
126
+ | Worker | 后厨 | TaskQueueProcessor |
127
+ | Redis | 保存号码和状态的系统 | 队列数据存储 |
128
+
129
+ 最容易误解的一点是:
130
+
131
+ ```ts
132
+ await taskQueueService.enqueue(...)
133
+ ```
134
+
135
+ 只表示任务已经成功写入队列,不表示任务已经执行完成。
136
+
137
+ 将来 HTTP 接口投递异步任务时,通常返回:
138
+
139
+ ```json
140
+ {
141
+ "jobId": "some-job-id",
142
+ "status": "queued"
143
+ }
144
+ ```
145
+
146
+ HTTP 状态可使用 202 Accepted。前端不能直接连接 Redis。如果前端以后需要查询长任务进度,应再增加数据库任务表和查询接口;这不属于本次公共队列基础搭建。
147
+
148
+ ---
149
+
150
+ ## 2. 本次范围
151
+
152
+ ### 2.1 本次包含
153
+
154
+ - 安装 BullMQ 依赖
155
+ - 队列专用环境配置
156
+ - 队列配置校验
157
+ - 默认物理队列注册
158
+ - 类型安全的任务名称与 Payload 契约
159
+ - 公共投递服务
160
+ - 公共 Worker
161
+ - 重试、指数退避和任务保留上限
162
+ - Worker 启停开关
163
+ - 优雅停机
164
+ - 单元测试和真实 Redis 冒烟测试
165
+ - 后续新任务接入模板
166
+
167
+ ### 2.2 本次不包含
168
+
169
+ - 上传清理的真实实现
170
+ - 邮件、通知、AI、视频等真实业务任务
171
+ - 定时任务
172
+ - Bull Board 管理界面
173
+ - async_tasks 数据库表
174
+ - SSE 或 WebSocket 进度推送
175
+ - 独立 Worker 应用
176
+ - Transactional Outbox
177
+
178
+ 上传清理只作为后续附加项,详细说明见 [上传清理设计方案](../../02.上传与文件/上传清理/设计方案.md),当前源码尚未实现该业务。
179
+
180
+ ---
181
+
182
+ ## 3. 历史项目基线(2026-09-03)
183
+
184
+ 下表记录公共队列实施前的源码状态,仅用于理解当时为何需要本方案;判断当前能力时应以源码和本文开头的当前事实复核为准。
185
+
186
+ | 项目 | 当时状态 | 本方案处理 |
187
+ | ------------------ | ------------------------- | ------------------------------------- |
188
+ | NestJS | 11.1.x,Fastify | 使用 @nestjs/bullmq 12 |
189
+ | Redis | Redis 8,已开启 AOF | 本地复用 Redis,队列使用 DB 1 |
190
+ | Session | 使用 Redis DB 0 | 与队列配置分开 |
191
+ | Session Redis 连接 | 面向 HTTP 快速失败 | 绝不把现有 Redis Client 注入 BullMQ |
192
+ | BullMQ | 尚未安装 | 加入 workspace catalog 和 server 依赖 |
193
+ | Shutdown Hooks | main.ts 尚未开启 | 增加 app.enableShutdownHooks() |
194
+ | Server 测试 | 只匹配 test/\*_/_.spec.ts | 队列测试必须放 apps/server/test |
195
+ | 环境文件 | 已有分层加载 | 沿用现有 ENV_ARR 顺序 |
196
+
197
+ 当前 Session Redis 使用了 commandTimeout、maxRetriesPerRequest 和 enableOfflineQueue 等请求链路策略。Worker 需要长期阻塞取任务和断线恢复,因此不能复用该 Redis 实例,也不要把这些参数原样复制到 BullMQ。
198
+
199
+ BullMQ 应接收一份普通 Redis 连接配置对象,由它为 Queue 和 Worker 创建、管理各自的连接。
200
+
201
+ ---
202
+
203
+ ## 4. 最终文件结构
204
+
205
+ 完成本方案后,建议形成:
206
+
207
+ ```text
208
+ apps/server/src/config/
209
+ └── queue.config.ts
210
+
211
+ apps/server/src/modules/task-queue/
212
+ ├── queue-worker.bootstrap.ts
213
+ ├── task-queue.constants.ts
214
+ ├── task-queue.contracts.ts
215
+ ├── task-queue.module.ts
216
+ ├── task-queue.processor.ts
217
+ └── task-queue.service.ts
218
+
219
+ apps/server/test/
220
+ ├── task-queue.processor.spec.ts
221
+ ├── task-queue.service.spec.ts
222
+ └── task-queue.integration.spec.ts
223
+ ```
224
+
225
+ 同时修改:
226
+
227
+ ```text
228
+ pnpm-workspace.yaml
229
+ apps/server/package.json
230
+ apps/server/.env.development
231
+ apps/server/.env.production
232
+ apps/server/src/config/index.ts
233
+ apps/server/src/app.module.ts
234
+ apps/server/src/main.ts
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 5. 公共队列必须先定下的规则
240
+
241
+ ### 5.1 队列名
242
+
243
+ 第一版只有一个物理队列:
244
+
245
+ ```text
246
+ server-tasks
247
+ ```
248
+
249
+ ### 5.2 任务名
250
+
251
+ 任务名统一采用:
252
+
253
+ ```text
254
+ 领域.动作.v版本
255
+ ```
256
+
257
+ 示例:
258
+
259
+ ```text
260
+ system.queue-smoke.v1
261
+ email.send.v1
262
+ notification.push.v1
263
+ ```
264
+
265
+ Payload 或行为发生不兼容修改时,必须升级任务版本,不能悄悄改变旧任务含义。
266
+
267
+ ### 5.3 Payload
268
+
269
+ Payload 只传:
270
+
271
+ - 数据库记录 ID
272
+ - 对象 ID
273
+ - 少量执行参数
274
+ - 必要的追踪字段
275
+
276
+ 禁止传:
277
+
278
+ - Buffer
279
+ - 文件正文
280
+ - 大段 Base64
281
+ - 完整数据库实体
282
+ - 登录 Token
283
+ - Redis、数据库或对象存储密钥
284
+ - 无法 JSON 序列化的数据
285
+
286
+ Worker 真正执行时,应根据 ID 重新读取数据库,不能相信队列中保存的是最新业务状态。
287
+
288
+ ### 5.4 投递和执行语义
289
+
290
+ BullMQ 是“至少执行一次”,不是“恰好执行一次”。
291
+
292
+ 同一个任务可能因为进程崩溃、锁丢失、网络中断或调用方重试而再次执行。因此每个真实任务处理器都必须幂等。
293
+
294
+ 常见幂等手段:
295
+
296
+ - 数据库唯一约束
297
+ - 带当前状态条件的 UPDATE
298
+ - 独立幂等键
299
+ - 调用外部服务时传稳定幂等键
300
+ - 发现目标已经完成时直接成功返回
301
+
302
+ 自定义 jobId 只能降低重复投递概率,不能替代业务幂等。
303
+
304
+ ### 5.5 每个进程中,一个物理队列只注册一种公共分发 Processor
305
+
306
+ 不要给同一个 server-tasks 队列中的每个任务分别写一个 @Processor。
307
+
308
+ 错误理解:
309
+
310
+ ```text
311
+ EmailProcessor 只会拿 email.send.v1
312
+ UploadProcessor 只会拿 upload.cleanup.v1
313
+ ```
314
+
315
+ 实际上,多个 Worker 会竞争同一队列中的所有 Job,并不会按 job.name 自动绑定。EmailProcessor 可能拿到上传任务。
316
+
317
+ 正确做法:
318
+
319
+ ```text
320
+ server-tasks
321
+ │
322
+ ▼
323
+ 唯一一种 TaskQueueProcessor
324
+ (每个启用 Worker 的副本各有一个同构实例)
325
+ │
326
+ ├── job.name = email.send.v1
327
+ ├── job.name = notification.push.v1
328
+ └── job.name = ...
329
+ ```
330
+
331
+ 第一版在公共 Processor 中分发。横向扩容时可以运行多个同构 Worker 实例;关键是它们都必须认识该物理队列中的全部 job.name,不能按任务名拆成互不相识的 Processor 类型。
332
+
333
+ 任务数量明显增加后,可再升级成 Handler Registry,但仍保持每个进程只注册一种能处理全部任务名的公共 WorkerHost。
334
+
335
+ ---
336
+
337
+ ## 6. 第 1 步:安装依赖
338
+
339
+ ### 本步目标
340
+
341
+ 把 BullMQ 加入 workspace catalog,并让 Server 使用 catalog 版本。
342
+
343
+ ### 文件位置
344
+
345
+ ```text
346
+ pnpm-workspace.yaml
347
+ apps/server/package.json
348
+ ```
349
+
350
+ ### 精确修改
351
+
352
+ 在 pnpm-workspace.yaml 的 catalog 中加入:
353
+
354
+ ```yaml
355
+ '@nestjs/bullmq': ^12.0.0
356
+ bullmq: ^6.3.4
357
+ ```
358
+
359
+ 在 apps/server/package.json 的 dependencies 中加入:
360
+
361
+ ```json
362
+ {
363
+ "@nestjs/bullmq": "catalog:",
364
+ "bullmq": "catalog:"
365
+ }
366
+ ```
367
+
368
+ ### 验证
369
+
370
+ 在仓库根目录执行:
371
+
372
+ ```powershell
373
+ vp install
374
+ vp run --filter server build
375
+ ```
376
+
377
+ ### 常见错误
378
+
379
+ - 安装成 @nestjs/bull。
380
+ - 只安装 @nestjs/bullmq,没有安装 bullmq。
381
+ - 在 Server 中写死版本,没有使用 catalog。
382
+ - 同时混用 Bull 和 BullMQ 的装饰器。
383
+
384
+ ---
385
+
386
+ ## 7. 第 2 步:增加队列环境变量
387
+
388
+ ### 本步目标
389
+
390
+ 让队列拥有独立于 Session Redis 的配置入口。
391
+
392
+ ### 文件位置
393
+
394
+ 开发环境建议修改:
395
+
396
+ ```text
397
+ apps/server/.env.development
398
+ ```
399
+
400
+ 个人本机密码等值应放:
401
+
402
+ ```text
403
+ apps/server/.env.development.local
404
+ ```
405
+
406
+ 生产变量写入部署平台 Secret,不要把真实密码提交到 Git。
407
+
408
+ 当前项目环境文件优先级为:
409
+
410
+ ```text
411
+ .env.<环境>.local
412
+ → .env.local
413
+ → .env.<环境>
414
+ → .env
415
+ ```
416
+
417
+ ### 建议变量
418
+
419
+ ```dotenv
420
+ # BullMQ 基础连接
421
+ QUEUE_REDIS_HOST=localhost
422
+ QUEUE_REDIS_PORT=6379
423
+ QUEUE_REDIS_USERNAME=
424
+ # 下面只是占位值,必须替换;不能原样复制使用
425
+ QUEUE_REDIS_PASSWORD=YOUR_LOCAL_REDIS_PASSWORD
426
+ QUEUE_REDIS_DB=1
427
+ QUEUE_PREFIX=bubbles:development:queue
428
+
429
+ # 当前 NestJS 进程是否启动 Worker
430
+ QUEUE_WORKER_ENABLED=true
431
+
432
+ # 默认任务策略
433
+ QUEUE_DEFAULT_ATTEMPTS=3
434
+ QUEUE_BACKOFF_DELAY_MS=1000
435
+ QUEUE_COMPLETED_AGE_SECONDS=86400
436
+ QUEUE_COMPLETED_COUNT=1000
437
+ QUEUE_FAILED_AGE_SECONDS=604800
438
+ QUEUE_FAILED_COUNT=5000
439
+ ```
440
+
441
+ 生产建议:
442
+
443
+ ```dotenv
444
+ # 下面两个 YOUR_... 都只是占位值,必须由部署配置或 Secret 替换
445
+ QUEUE_REDIS_HOST=YOUR_QUEUE_REDIS_HOST
446
+ QUEUE_REDIS_PORT=6379
447
+ QUEUE_REDIS_USERNAME=
448
+ QUEUE_REDIS_PASSWORD=YOUR_PRODUCTION_QUEUE_REDIS_PASSWORD
449
+ QUEUE_REDIS_DB=0
450
+ QUEUE_PREFIX=bubbles:production:queue
451
+ QUEUE_WORKER_ENABLED=true
452
+ ```
453
+
454
+ ### 代码解释
455
+
456
+ - 本地 Session 使用 DB 0,队列使用 DB 1,便于查看和清理。
457
+ - Redis DB 只是逻辑隔离,不是性能、安全或故障隔离。
458
+ - 生产推荐队列使用独立 Redis,并使用 DB 0。
459
+ - Redis Cluster 只支持 DB 0。
460
+ - QUEUE_PREFIX 用于区分项目和环境。
461
+ - 不要配置 ioredis 的 keyPrefix。BullMQ 必须使用自己的 prefix。
462
+ - 不要把字符串 false 写成 Boolean(process.env.QUEUE_WORKER_ENABLED),因为 Boolean('false') 仍然是 true。
463
+
464
+ ### 验证
465
+
466
+ 确认本地 Redis 可用。把命令中的占位密码替换为自己的本地值:
467
+
468
+ ```powershell
469
+ docker compose up -d redis
470
+ $queueRedisPassword = Read-Host '请输入本地 Redis 密码'
471
+ docker compose exec -e REDISCLI_AUTH=$queueRedisPassword redis redis-cli -n 1 ping
472
+ Remove-Variable queueRedisPassword
473
+ ```
474
+
475
+ 预期输出:
476
+
477
+ ```text
478
+ PONG
479
+ ```
480
+
481
+ ### 常见错误
482
+
483
+ - 队列仍然使用 REDIS_DB=0,和 Session 键混在一起。
484
+ - 把生产密码写入 .env.production 并提交。
485
+ - Prefix 在开发、测试、生产环境完全相同。
486
+ - 使用 Redis Cluster 时仍配置 DB 1。
487
+
488
+ ---
489
+
490
+ ## 8. 第 3 步:建立 queue.config.ts
491
+
492
+ ### 本步目标
493
+
494
+ 集中读取并校验所有队列变量。配置错误时让应用启动失败,而不是运行到一半才发现。
495
+
496
+ ### 文件位置
497
+
498
+ 新建:
499
+
500
+ ```text
501
+ apps/server/src/config/queue.config.ts
502
+ ```
503
+
504
+ ### 完整代码
505
+
506
+ ```ts
507
+ import { registerAs } from '@nestjs/config'
508
+
509
+ export interface QueueConfig {
510
+ host: string
511
+ port: number
512
+ username?: string
513
+ password?: string
514
+ db: number
515
+ prefix: string
516
+ workerEnabled: boolean
517
+ defaultAttempts: number
518
+ backoffDelayMs: number
519
+ completedAgeSeconds: number
520
+ completedCount: number
521
+ failedAgeSeconds: number
522
+ failedCount: number
523
+ }
524
+
525
+ function readInteger(
526
+ name: string,
527
+ fallback: number,
528
+ minimum: number,
529
+ maximum = Number.MAX_SAFE_INTEGER,
530
+ ) {
531
+ const rawValue = (process.env[name] ?? String(fallback)).trim()
532
+ const value = Number(rawValue)
533
+
534
+ if (rawValue === '' || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
535
+ throw new Error(name + ' must be an integer between ' + minimum + ' and ' + maximum)
536
+ }
537
+
538
+ return value
539
+ }
540
+
541
+ function readBoolean(name: string, fallback: boolean) {
542
+ const rawValue = process.env[name]
543
+
544
+ if (rawValue === undefined) {
545
+ return fallback
546
+ }
547
+
548
+ const value = rawValue.trim().toLowerCase()
549
+
550
+ if (value === 'true') {
551
+ return true
552
+ }
553
+
554
+ if (value === 'false') {
555
+ return false
556
+ }
557
+
558
+ throw new Error(name + ' must be true or false')
559
+ }
560
+
561
+ function readOptionalText(name: string) {
562
+ const value = process.env[name]?.trim()
563
+ return value ? value : undefined
564
+ }
565
+
566
+ export default registerAs('queue', (): QueueConfig => {
567
+ const environment = process.env.NODE_ENV ?? 'development'
568
+ const prefix = process.env.QUEUE_PREFIX?.trim() || ['bubbles', environment, 'queue'].join(':')
569
+
570
+ if (!prefix) {
571
+ throw new Error('QUEUE_PREFIX must not be empty')
572
+ }
573
+
574
+ return {
575
+ host: process.env.QUEUE_REDIS_HOST?.trim() || 'localhost',
576
+ port: readInteger('QUEUE_REDIS_PORT', 6379, 1, 65_535),
577
+ username: readOptionalText('QUEUE_REDIS_USERNAME'),
578
+ password: readOptionalText('QUEUE_REDIS_PASSWORD'),
579
+ db: readInteger('QUEUE_REDIS_DB', 1, 0),
580
+ prefix,
581
+ workerEnabled: readBoolean('QUEUE_WORKER_ENABLED', true),
582
+ defaultAttempts: readInteger('QUEUE_DEFAULT_ATTEMPTS', 3, 1),
583
+ backoffDelayMs: readInteger('QUEUE_BACKOFF_DELAY_MS', 1_000, 1),
584
+ completedAgeSeconds: readInteger('QUEUE_COMPLETED_AGE_SECONDS', 86_400, 1),
585
+ completedCount: readInteger('QUEUE_COMPLETED_COUNT', 1_000, 1),
586
+ failedAgeSeconds: readInteger('QUEUE_FAILED_AGE_SECONDS', 604_800, 1),
587
+ failedCount: readInteger('QUEUE_FAILED_COUNT', 5_000, 1),
588
+ }
589
+ })
590
+ ```
591
+
592
+ ### 代码解释
593
+
594
+ - 所有数字先转成整数,再做范围校验。
595
+ - 密码和用户名允许为空,方便连接无认证的开发 Redis。
596
+ - QUEUE_WORKER_ENABLED 被严格解析成 true 或 false。
597
+ - 默认 Prefix 自动带 NODE_ENV,避免不同环境撞键。
598
+ - 配置文件只负责读取配置,不创建 Redis Client。
599
+
600
+ ### 常见错误
601
+
602
+ - 使用 Number.parseInt 后不检查 NaN。
603
+ - 用 Boolean('false') 解析环境变量。
604
+ - 在配置文件中直接 new Redis。
605
+ - 把 Session Redis 配置对象整个复制过来。
606
+
607
+ ---
608
+
609
+ ## 9. 第 4 步:注册 queueConfig
610
+
611
+ ### 本步目标
612
+
613
+ 让 ConfigService 可以读取 queue 配置。
614
+
615
+ ### 文件位置
616
+
617
+ 修改:
618
+
619
+ ```text
620
+ apps/server/src/config/index.ts
621
+ apps/server/src/app.module.ts
622
+ ```
623
+
624
+ ### 精确修改
625
+
626
+ 在 apps/server/src/config/index.ts 增加:
627
+
628
+ ```ts
629
+ export { default as queueConfig } from './queue.config'
630
+ ```
631
+
632
+ 在 AppModule 顶部的配置导入中加入 queueConfig:
633
+
634
+ ```ts
635
+ import {
636
+ appConfig,
637
+ databaseConfig,
638
+ llmConfig,
639
+ queueConfig,
640
+ redisConfig,
641
+ sessionConfig,
642
+ storageConfig,
643
+ } from '@/config'
644
+ ```
645
+
646
+ 把 ConfigModule.forRoot 的 load 改为:
647
+
648
+ ```ts
649
+ load: [
650
+ appConfig,
651
+ databaseConfig,
652
+ llmConfig,
653
+ queueConfig,
654
+ redisConfig,
655
+ sessionConfig,
656
+ storageConfig,
657
+ ],
658
+ ```
659
+
660
+ ### 验证
661
+
662
+ ```powershell
663
+ vp run --filter server build
664
+ ```
665
+
666
+ 如果 QUEUE_REDIS_PORT 等变量不合法,构建通常不会读取运行环境,但启动 Server 时必须立即报出清晰的配置错误。
667
+
668
+ ---
669
+
670
+ ## 10. 第 5 步:定义队列名和任务契约
671
+
672
+ ### 本步目标
673
+
674
+ 集中定义物理队列名、任务名和 Payload 类型,避免业务模块随手拼字符串。
675
+
676
+ ### 文件位置
677
+
678
+ 新建:
679
+
680
+ ```text
681
+ apps/server/src/modules/task-queue/task-queue.constants.ts
682
+ apps/server/src/modules/task-queue/task-queue.contracts.ts
683
+ ```
684
+
685
+ ### task-queue.constants.ts
686
+
687
+ ```ts
688
+ export const TASK_QUEUE_NAME = 'server-tasks'
689
+
690
+ export const TASK_QUEUE_CONCURRENCY = 5
691
+
692
+ export const TASK_JOB_NAMES = {
693
+ QUEUE_SMOKE: 'system.queue-smoke.v1',
694
+ } as const
695
+ ```
696
+
697
+ ### task-queue.contracts.ts
698
+
699
+ ```ts
700
+ import { z } from 'zod'
701
+ import { TASK_JOB_NAMES, TASK_QUEUE_NAME } from './task-queue.constants'
702
+
703
+ export const QueueSmokePayloadSchema = z.object({
704
+ marker: z.string().min(1).max(100),
705
+ enqueuedAt: z.string().datetime(),
706
+ })
707
+
708
+ export type TaskName = (typeof TASK_JOB_NAMES)[keyof typeof TASK_JOB_NAMES]
709
+
710
+ export interface TaskPayloadMap {
711
+ [TASK_JOB_NAMES.QUEUE_SMOKE]: z.infer<typeof QueueSmokePayloadSchema>
712
+ }
713
+
714
+ export const TASK_PAYLOAD_SCHEMAS = {
715
+ [TASK_JOB_NAMES.QUEUE_SMOKE]: QueueSmokePayloadSchema,
716
+ } satisfies Record<TaskName, z.ZodType>
717
+
718
+ export function parseTaskPayload<Name extends TaskName>(
719
+ name: Name,
720
+ payload: unknown,
721
+ ): TaskPayloadMap[Name] {
722
+ return TASK_PAYLOAD_SCHEMAS[name].parse(payload) as TaskPayloadMap[Name]
723
+ }
724
+
725
+ export interface EnqueueTaskOptions {
726
+ jobId?: string
727
+ delayMs?: number
728
+ }
729
+
730
+ export interface EnqueuedTask<Name extends TaskName = TaskName> {
731
+ queueName: typeof TASK_QUEUE_NAME
732
+ jobId: string
733
+ name: Name
734
+ }
735
+ ```
736
+
737
+ ### 代码解释
738
+
739
+ - TASK_QUEUE_NAME 是 Redis 中的物理队列。
740
+ - TASK_JOB_NAMES 是任务协议,不允许业务代码手写字符串。
741
+ - TaskPayloadMap 建立“任务名 → Payload 类型”的对应关系。
742
+ - TypeScript 保护正常后端调用。
743
+ - Zod 保护 Redis 中遗留的旧任务、错误生产者和运行时脏数据。
744
+ - 冒烟任务只用于验证基础设施,不承载业务。
745
+
746
+ ### 常见错误
747
+
748
+ - 任务名没有版本号。
749
+ - 多个文件分别定义相同字符串。
750
+ - 只写 TypeScript interface,不做 Worker 运行时校验。
751
+ - 在 Payload 中放完整文件内容。
752
+
753
+ ---
754
+
755
+ ## 11. 第 6 步:实现公共投递服务
756
+
757
+ ### 本步目标
758
+
759
+ 让所有业务模块只通过 TaskQueueService 投递任务,不直接操作 BullMQ Queue。
760
+
761
+ ### 文件位置
762
+
763
+ 新建:
764
+
765
+ ```text
766
+ apps/server/src/modules/task-queue/task-queue.service.ts
767
+ ```
768
+
769
+ ### 完整代码
770
+
771
+ ```ts
772
+ import { InjectQueue } from '@nestjs/bullmq'
773
+ import { Injectable } from '@nestjs/common'
774
+ import type { JobsOptions, Queue } from 'bullmq'
775
+ import { TASK_QUEUE_NAME } from './task-queue.constants'
776
+ import {
777
+ type EnqueuedTask,
778
+ type EnqueueTaskOptions,
779
+ parseTaskPayload,
780
+ type TaskName,
781
+ type TaskPayloadMap,
782
+ } from './task-queue.contracts'
783
+
784
+ @Injectable()
785
+ export class TaskQueueService {
786
+ constructor(
787
+ @InjectQueue(TASK_QUEUE_NAME)
788
+ private readonly queue: Queue,
789
+ ) {}
790
+
791
+ async enqueue<Name extends TaskName>(
792
+ name: Name,
793
+ payload: TaskPayloadMap[Name],
794
+ options: EnqueueTaskOptions = {},
795
+ ): Promise<EnqueuedTask<Name>> {
796
+ this.validateOptions(options)
797
+
798
+ const data = parseTaskPayload(name, payload)
799
+ const jobOptions: JobsOptions = {
800
+ ...(options.jobId === undefined ? {} : { jobId: options.jobId }),
801
+ ...(options.delayMs === undefined ? {} : { delay: options.delayMs }),
802
+ }
803
+
804
+ const job = await this.queue.add(name, data, jobOptions)
805
+
806
+ if (job.id === undefined) {
807
+ throw new Error('BullMQ returned a Job without an id')
808
+ }
809
+
810
+ return {
811
+ queueName: TASK_QUEUE_NAME,
812
+ jobId: job.id,
813
+ name,
814
+ }
815
+ }
816
+
817
+ private validateOptions(options: EnqueueTaskOptions) {
818
+ if (options.jobId?.includes(':')) {
819
+ throw new Error('BullMQ jobId must not contain a colon')
820
+ }
821
+
822
+ if (
823
+ options.delayMs !== undefined &&
824
+ (!Number.isSafeInteger(options.delayMs) || options.delayMs < 0)
825
+ ) {
826
+ throw new Error('delayMs must be a non-negative safe integer')
827
+ }
828
+ }
829
+ }
830
+ ```
831
+
832
+ ### 代码解释
833
+
834
+ - 业务层拿不到 pause、clean、drain、obliterate 等危险方法。
835
+ - 对外只返回 queueName、jobId 和 name,不泄漏整个 BullMQ Job。
836
+ - Service 在投递前做一次 Zod 校验。
837
+ - 默认重试和保留规则由公共模块统一提供,业务不能随意覆盖。
838
+ - jobId 不能包含冒号。
839
+
840
+ 不要在这里为 queue.add 层层添加 catch。未知 Redis 异常应继续向上交给项目现有全局异常过滤器。只有以后明确需要转换为某个公开 503 错误时,才在这个基础设施边界转换一次。
841
+
842
+ ### jobId 的边界
843
+
844
+ 相同 jobId 在旧 Job 仍保留于队列时,可以阻止重复加入。但 Job 被自动清理后,这个 jobId 可以再次投递。
845
+
846
+ 因此:
847
+
848
+ ```text
849
+ jobId 去重 ≠ 业务恰好执行一次
850
+ ```
851
+
852
+ 真实处理器仍必须使用数据库约束或状态条件保证幂等。
853
+
854
+ ---
855
+
856
+ ## 12. 第 7 步:实现公共分发 Worker
857
+
858
+ ### 本步目标
859
+
860
+ 在每个启用 Worker 的进程中,为 server-tasks 建立唯一一种公共 WorkerHost,并先支持基础设施冒烟任务。
861
+
862
+ ### 文件位置
863
+
864
+ 新建:
865
+
866
+ ```text
867
+ apps/server/src/modules/task-queue/task-queue.processor.ts
868
+ ```
869
+
870
+ ### 完整代码
871
+
872
+ ```ts
873
+ import { OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'
874
+ import { Logger } from '@nestjs/common'
875
+ import { type Job, UnrecoverableError } from 'bullmq'
876
+ import { TASK_JOB_NAMES, TASK_QUEUE_CONCURRENCY, TASK_QUEUE_NAME } from './task-queue.constants'
877
+ import { QueueSmokePayloadSchema } from './task-queue.contracts'
878
+
879
+ @Processor(TASK_QUEUE_NAME, { concurrency: TASK_QUEUE_CONCURRENCY })
880
+ export class TaskQueueProcessor extends WorkerHost {
881
+ private readonly logger = new Logger(TaskQueueProcessor.name)
882
+
883
+ async process(job: Job): Promise<unknown> {
884
+ switch (job.name) {
885
+ case TASK_JOB_NAMES.QUEUE_SMOKE: {
886
+ const parsed = QueueSmokePayloadSchema.safeParse(job.data)
887
+
888
+ if (!parsed.success) {
889
+ throw new UnrecoverableError('Invalid payload for ' + job.name)
890
+ }
891
+
892
+ return {
893
+ marker: parsed.data.marker,
894
+ processedAt: new Date().toISOString(),
895
+ }
896
+ }
897
+
898
+ default:
899
+ throw new UnrecoverableError('Unsupported task name: ' + job.name)
900
+ }
901
+ }
902
+
903
+ @OnWorkerEvent('completed')
904
+ onCompleted(job: Job) {
905
+ this.logger.log({
906
+ event: 'queue_job_completed',
907
+ queueName: TASK_QUEUE_NAME,
908
+ jobId: job.id,
909
+ jobName: job.name,
910
+ attemptsMade: job.attemptsMade,
911
+ })
912
+ }
913
+
914
+ @OnWorkerEvent('failed')
915
+ onFailed(job: Job | undefined, error: Error) {
916
+ this.logger.error({
917
+ event: 'queue_job_failed',
918
+ queueName: TASK_QUEUE_NAME,
919
+ jobId: job?.id,
920
+ jobName: job?.name,
921
+ attemptsMade: job?.attemptsMade,
922
+ errorName: error.name,
923
+ })
924
+ }
925
+
926
+ @OnWorkerEvent('error')
927
+ onWorkerError(error: Error) {
928
+ this.logger.error({
929
+ event: 'queue_worker_error',
930
+ queueName: TASK_QUEUE_NAME,
931
+ errorName: error.name,
932
+ })
933
+ }
934
+ }
935
+ ```
936
+
937
+ ### 代码解释
938
+
939
+ - concurrency: 5 表示单个 NestJS 实例最多同时执行 5 个任务。
940
+ - 冒烟任务返回 marker 和处理时间,只验证完整链路。
941
+ - 未知任务名和非法 Payload 不会因为重试而自动变正确,所以使用 UnrecoverableError。
942
+ - 真实任务遇到数据库暂时不可用、第三方超时等可恢复错误时,不要吞掉异常,让 BullMQ 按公共策略重试。
943
+ - 日志只记录任务元数据,不记录完整 Payload。
944
+ - 第一版也不直接记录原始 error.message 和 error.stack,避免第三方 SDK、SQL 或连接错误把敏感信息带进日志。以后如需完整错误诊断,应先接入统一的日志脱敏函数。
945
+
946
+ ### 绝对不要这样写
947
+
948
+ ```ts
949
+ try {
950
+ await doRealWork()
951
+ } catch (error) {
952
+ return { ok: false }
953
+ }
954
+ ```
955
+
956
+ 这样 Worker 会把失败任务标记为 completed,BullMQ 不会重试。
957
+
958
+ 正确做法是让异常继续抛出:
959
+
960
+ ```ts
961
+ await doRealWork()
962
+ return { ok: true }
963
+ ```
964
+
965
+ ### 多实例并发
966
+
967
+ 如果部署 3 个 API 副本,并且每个副本都启用 Worker:
968
+
969
+ ```text
970
+ 总并发 = 3 × 5 = 15
971
+ ```
972
+
973
+ 这不是错误,但上线前必须清楚总并发会放大。
974
+
975
+ ---
976
+
977
+ ## 13. 第 8 步:增加 Worker 启动开关
978
+
979
+ ### 本步目标
980
+
981
+ 同一套代码既可以“投递并消费”,也可以只投递不消费,为以后拆 Worker 进程留出入口。
982
+
983
+ ### 文件位置
984
+
985
+ 新建:
986
+
987
+ ```text
988
+ apps/server/src/modules/task-queue/queue-worker.bootstrap.ts
989
+ ```
990
+
991
+ ### 完整代码
992
+
993
+ ```ts
994
+ import { BullRegistrar } from '@nestjs/bullmq'
995
+ import { Injectable, type OnApplicationBootstrap } from '@nestjs/common'
996
+ import { ConfigService } from '@nestjs/config'
997
+
998
+ @Injectable()
999
+ export class QueueWorkerBootstrap implements OnApplicationBootstrap {
1000
+ private registered = false
1001
+
1002
+ constructor(
1003
+ private readonly config: ConfigService,
1004
+ private readonly registrar: BullRegistrar,
1005
+ ) {}
1006
+
1007
+ onApplicationBootstrap() {
1008
+ const workerEnabled = this.config.getOrThrow<boolean>('queue.workerEnabled')
1009
+
1010
+ if (!workerEnabled || this.registered) {
1011
+ return
1012
+ }
1013
+
1014
+ this.registrar.register()
1015
+ this.registered = true
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ ### 代码解释
1021
+
1022
+ - @nestjs/bullmq 默认会自动启动所有 Processor。
1023
+ - 下一步会开启 manualRegistration,改为由 QueueWorkerBootstrap 决定是否启动。
1024
+ - onApplicationBootstrap 发生在模块初始化之后,配置已经可读取。
1025
+ - 不要在 @Processor 装饰器参数中直接读取 .env。
1026
+
1027
+ ---
1028
+
1029
+ ## 14. 第 9 步:组装 TaskQueueModule
1030
+
1031
+ ### 本步目标
1032
+
1033
+ 统一注册 BullMQ、默认队列、公共 Service 和 Worker。
1034
+
1035
+ ### 文件位置
1036
+
1037
+ 新建:
1038
+
1039
+ ```text
1040
+ apps/server/src/modules/task-queue/task-queue.module.ts
1041
+ ```
1042
+
1043
+ ### 完整代码
1044
+
1045
+ ```ts
1046
+ import { BullModule } from '@nestjs/bullmq'
1047
+ import { Module } from '@nestjs/common'
1048
+ import { ConfigModule, ConfigService } from '@nestjs/config'
1049
+ import type { QueueConfig } from '@/config/queue.config'
1050
+ import { QueueWorkerBootstrap } from './queue-worker.bootstrap'
1051
+ import { TASK_QUEUE_NAME } from './task-queue.constants'
1052
+ import { TaskQueueProcessor } from './task-queue.processor'
1053
+ import { TaskQueueService } from './task-queue.service'
1054
+
1055
+ @Module({
1056
+ imports: [
1057
+ BullModule.forRootAsync({
1058
+ imports: [ConfigModule],
1059
+ inject: [ConfigService],
1060
+ useFactory: (config: ConfigService) => {
1061
+ const queue = config.getOrThrow<QueueConfig>('queue')
1062
+
1063
+ return {
1064
+ connection: {
1065
+ host: queue.host,
1066
+ port: queue.port,
1067
+ username: queue.username,
1068
+ password: queue.password,
1069
+ db: queue.db,
1070
+ connectTimeout: 10_000,
1071
+ },
1072
+ prefix: queue.prefix,
1073
+ defaultJobOptions: {
1074
+ attempts: queue.defaultAttempts,
1075
+ backoff: {
1076
+ type: 'exponential',
1077
+ delay: queue.backoffDelayMs,
1078
+ },
1079
+ removeOnComplete: {
1080
+ age: queue.completedAgeSeconds,
1081
+ count: queue.completedCount,
1082
+ },
1083
+ removeOnFail: {
1084
+ age: queue.failedAgeSeconds,
1085
+ count: queue.failedCount,
1086
+ },
1087
+ },
1088
+ }
1089
+ },
1090
+ extraOptions: {
1091
+ manualRegistration: true,
1092
+ },
1093
+ }),
1094
+ BullModule.registerQueue({
1095
+ name: TASK_QUEUE_NAME,
1096
+ }),
1097
+ ],
1098
+ providers: [QueueWorkerBootstrap, TaskQueueProcessor, TaskQueueService],
1099
+ exports: [TaskQueueService],
1100
+ })
1101
+ export class TaskQueueModule {}
1102
+ ```
1103
+
1104
+ ### 连接策略解释
1105
+
1106
+ 这份 connection 必须是普通配置对象,不能是现有 Session ioredis 实例。
1107
+
1108
+ 第一版单进程 Queue + Worker 建议:
1109
+
1110
+ - 设置 connectTimeout。
1111
+ - 不设置 commandTimeout。
1112
+ - 不设置 enableOfflineQueue。
1113
+ - 不显式设置 maxRetriesPerRequest。
1114
+
1115
+ BullMQ 会为 Worker 的阻塞连接自动使用适合长期消费的 maxRetriesPerRequest: null;普通 Queue 连接保留 ioredis 默认行为。
1116
+
1117
+ 如果以后真正拆成独立 Producer 和 Worker 进程,再进一步配置:
1118
+
1119
+ - API Producer:maxRetriesPerRequest: 1,快速失败。
1120
+ - Worker:maxRetriesPerRequest: null,持续重连。
1121
+
1122
+ 不要在当前共享配置里显式写 maxRetriesPerRequest: 1。BullMQ 虽然会为 Worker 覆盖它,但会输出警告。
1123
+
1124
+ ### manualRegistration 的作用范围
1125
+
1126
+ manualRegistration 配置在 BullModule.forRootAsync 上,是当前 Nest 应用中 BullMQ 根模块级的全局开关,不只控制 server-tasks。调用 BullRegistrar.register() 时,会注册应用内所有 BullMQ Processor 和 QueueEvents Listener。
1127
+
1128
+ 因此第一版的 QUEUE_WORKER_ENABLED 是“当前进程是否启动全部 BullMQ 消费者”的进程级开关。未来新增物理队列时,也会受到同一个开关控制;不要为了单独控制某个队列再创建第二套 BullModule.forRoot 或 forRootAsync 根配置。若以后确实需要不同队列独立启停,优先拆成独立 Worker 应用或重新设计更细粒度的消费者开关。
1129
+
1130
+ ### 默认任务策略解释
1131
+
1132
+ - attempts: 3 表示总共最多执行 3 次,不是“首次加重试 3 次”。
1133
+ - backoff 使用指数退避,避免外部服务故障时持续猛打。
1134
+ - removeOnComplete 同时限制保留时间和数量。
1135
+ - removeOnFail 保留更久,方便排查,但同样设置数量上限。
1136
+ - 没有保留上限会让 Redis 数据持续增长。
1137
+
1138
+ ### 为什么暂时不加 @Global
1139
+
1140
+ 需要投递任务的业务模块应显式导入 TaskQueueModule:
1141
+
1142
+ ```ts
1143
+ @Module({
1144
+ imports: [TaskQueueModule],
1145
+ })
1146
+ export class SomeBusinessModule {}
1147
+ ```
1148
+
1149
+ 这样依赖更清晰,测试时也更容易看出模块缺少什么。
1150
+
1151
+ ### 不需要的旧组件
1152
+
1153
+ BullMQ 当前版本不需要额外创建 QueueScheduler。不要复制旧教程中的 QueueScheduler 或 @Process 写法。
1154
+
1155
+ ---
1156
+
1157
+ ## 15. 第 10 步:接入 AppModule
1158
+
1159
+ ### 本步目标
1160
+
1161
+ 让应用启动时创建 Queue,并根据开关决定是否启动 Worker。
1162
+
1163
+ ### 文件位置
1164
+
1165
+ 修改:
1166
+
1167
+ ```text
1168
+ apps/server/src/app.module.ts
1169
+ ```
1170
+
1171
+ ### 精确修改
1172
+
1173
+ 增加导入:
1174
+
1175
+ ```ts
1176
+ import { TaskQueueModule } from './modules/task-queue/task-queue.module'
1177
+ ```
1178
+
1179
+ 在 imports 中加入:
1180
+
1181
+ ```ts
1182
+ TaskQueueModule,
1183
+ ```
1184
+
1185
+ 建议放在 RedisModule 之后、具体业务模块之前,便于阅读:
1186
+
1187
+ ```ts
1188
+ imports: [
1189
+ ConfigModule.forRoot(...),
1190
+ DatabaseModule,
1191
+ RedisModule.forRootAsync(...),
1192
+ TaskQueueModule,
1193
+ TestRedisModule,
1194
+ TestDbModule,
1195
+ AuthModule,
1196
+ UploadModule,
1197
+ ],
1198
+ ```
1199
+
1200
+ TaskQueueModule 只需在 AppModule 中接入一次以确保 Worker 启动。需要投递任务的具体业务模块仍显式导入它,以获得 TaskQueueService。
1201
+
1202
+ ---
1203
+
1204
+ ## 16. 第 11 步:开启优雅停机
1205
+
1206
+ ### 本步目标
1207
+
1208
+ 让 NestJS 在收到 SIGTERM 或 SIGINT 时关闭 Queue 和 Worker,不再领取新任务,并等待当前任务正常结束。
1209
+
1210
+ ### 文件位置
1211
+
1212
+ 修改:
1213
+
1214
+ ```text
1215
+ apps/server/src/main.ts
1216
+ ```
1217
+
1218
+ ### 精确修改
1219
+
1220
+ 在 NestFactory.create 完成后增加:
1221
+
1222
+ ```ts
1223
+ app.enableShutdownHooks()
1224
+ ```
1225
+
1226
+ 建议位置:
1227
+
1228
+ ```ts
1229
+ const app = await NestFactory.create<NestFastifyApplication>(AppModule, createFastifyAdapter(), {
1230
+ logger: new ConsoleLogger({
1231
+ json: process.env.NODE_ENV === 'production',
1232
+ colors: process.env.NODE_ENV === 'development',
1233
+ }),
1234
+ })
1235
+
1236
+ app.enableShutdownHooks()
1237
+
1238
+ app.enableCors({
1239
+ // 保留当前配置
1240
+ })
1241
+ ```
1242
+
1243
+ ### 代码解释
1244
+
1245
+ - @nestjs/bullmq 会关闭自己注册的 Queue。
1246
+ - Worker.close() 会停止领取新 Job,并等待当前 Job 结束。
1247
+ - 部署平台的 termination grace period 必须大于普通任务的最长执行时间。
1248
+ - 不需要再重复编写 process.on('SIGTERM')。
1249
+
1250
+ 如果进程最终仍被强杀,正在执行的任务之后可能被判定为 stalled 并再次执行,这也是处理器必须幂等的原因。
1251
+
1252
+ ---
1253
+
1254
+ ## 17. 第 12 步:增加单元测试
1255
+
1256
+ ### 本步目标
1257
+
1258
+ 不连接 Redis,先验证公共 Service 和 Worker 分发规则。
1259
+
1260
+ ### 文件位置
1261
+
1262
+ 新建:
1263
+
1264
+ ```text
1265
+ apps/server/test/task-queue.service.spec.ts
1266
+ apps/server/test/task-queue.processor.spec.ts
1267
+ ```
1268
+
1269
+ 注意:当前 Server 的 Vite+ 测试配置只匹配 test/\*_/_.spec.ts。不要把测试放在 src 目录。
1270
+
1271
+ ### TaskQueueService 至少测试
1272
+
1273
+ - 合法任务被传给 queue.add。
1274
+ - jobId 和 delayMs 被正确转换。
1275
+ - 返回值不暴露完整 Job。
1276
+ - jobId 含冒号时拒绝。
1277
+ - delayMs 为负数或非安全整数时拒绝。
1278
+ - Payload 不合法时拒绝。
1279
+
1280
+ 测试可直接模拟 Queue:
1281
+
1282
+ ```ts
1283
+ import type { Queue } from 'bullmq'
1284
+ import { describe, expect, it, vi } from 'vite-plus/test'
1285
+ import { TASK_JOB_NAMES } from '@/modules/task-queue/task-queue.constants'
1286
+ import { TaskQueueService } from '@/modules/task-queue/task-queue.service'
1287
+
1288
+ describe('TaskQueueService', () => {
1289
+ it('enqueues a typed task', async () => {
1290
+ const add = vi.fn().mockResolvedValue({
1291
+ id: 'queue-smoke-1',
1292
+ })
1293
+ const queue = { add } as unknown as Queue
1294
+ const service = new TaskQueueService(queue)
1295
+
1296
+ const result = await service.enqueue(
1297
+ TASK_JOB_NAMES.QUEUE_SMOKE,
1298
+ {
1299
+ marker: 'test',
1300
+ enqueuedAt: new Date().toISOString(),
1301
+ },
1302
+ {
1303
+ jobId: 'queue-smoke-1',
1304
+ delayMs: 100,
1305
+ },
1306
+ )
1307
+
1308
+ expect(add).toHaveBeenCalledOnce()
1309
+ expect(result).toEqual({
1310
+ queueName: 'server-tasks',
1311
+ jobId: 'queue-smoke-1',
1312
+ name: TASK_JOB_NAMES.QUEUE_SMOKE,
1313
+ })
1314
+ })
1315
+ })
1316
+ ```
1317
+
1318
+ ### TaskQueueProcessor 至少测试
1319
+
1320
+ - 合法冒烟任务返回 marker 和 processedAt。
1321
+ - 非法 Payload 抛出 UnrecoverableError。
1322
+ - 未知任务名抛出 UnrecoverableError。
1323
+ - 普通业务异常不会被吞掉。
1324
+
1325
+ ### 验证
1326
+
1327
+ ```powershell
1328
+ vp run --filter server test
1329
+ ```
1330
+
1331
+ ---
1332
+
1333
+ ## 18. 第 13 步:增加真实 Redis 冒烟测试
1334
+
1335
+ ### 本步目标
1336
+
1337
+ 证明真正的“投递 → 消费 → 完成”链路可用。只 Mock Queue 不能证明 Redis 和 Worker 已正确接通。
1338
+
1339
+ ### 文件位置
1340
+
1341
+ 新建:
1342
+
1343
+ ```text
1344
+ apps/server/test/task-queue.integration.spec.ts
1345
+ ```
1346
+
1347
+ ### 为什么默认跳过
1348
+
1349
+ 这个文件仍使用 `.spec.ts` 后缀,所以普通 `vp run --filter server test` 和根目录 `vp run ready` 都能发现它。为了避免没有启动 Redis 时单元测试整体失败,真实 Redis 测试必须通过 `RUN_QUEUE_INTEGRATION=true` 显式开启;未设置时使用 `describe.skip`。
1350
+
1351
+ ### 测试流程
1352
+
1353
+ ```text
1354
+ 生成唯一测试 Prefix
1355
+ ↓
1356
+ 创建 ConfigModule + TaskQueueModule
1357
+ ↓
1358
+ 等待 TestingModule 完成初始化
1359
+ ↓
1360
+ 创建 QueueEvents 并等待 ready
1361
+ ↓
1362
+ 通过 TaskQueueService 投递 system.queue-smoke.v1
1363
+ ↓
1364
+ 根据 jobId 取得 Job
1365
+ ↓
1366
+ 最多等待 5~10 秒
1367
+ ↓
1368
+ 断言状态为 completed,并校验返回 marker
1369
+ ↓
1370
+ 关闭 QueueEvents
1371
+ ↓
1372
+ 关闭 TestingModule
1373
+ ```
1374
+
1375
+ 测试 Prefix 必须唯一,例如:
1376
+
1377
+ ```text
1378
+ bubbles:test:<随机UUID>:queue
1379
+ ```
1380
+
1381
+ 不要让集成测试连接开发或生产 Prefix 后执行 drain、clean 或 obliterate。
1382
+
1383
+ ### 完整测试文件
1384
+
1385
+ 因为公共模块使用了 manualRegistration: true,只执行 compile() 不会启动 Worker。测试必须显式执行 testingModule.init(),让 QueueWorkerBootstrap 的 onApplicationBootstrap 得到调用。
1386
+
1387
+ 同时必须使用 ConfigModule.forRoot 注册 queueConfig,并且要在编译模块之前设置唯一测试 Prefix。下面是一份可以直接复制的完整文件:
1388
+
1389
+ ```ts
1390
+ import { randomUUID } from 'node:crypto'
1391
+ import { getQueueToken } from '@nestjs/bullmq'
1392
+ import { ConfigModule } from '@nestjs/config'
1393
+ import { Test, type TestingModule } from '@nestjs/testing'
1394
+ import { type Queue, QueueEvents } from 'bullmq'
1395
+ import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test'
1396
+ import queueConfig from '@/config/queue.config'
1397
+ import { TaskQueueModule } from '@/modules/task-queue/task-queue.module'
1398
+ import { TaskQueueService } from '@/modules/task-queue/task-queue.service'
1399
+ import { TASK_JOB_NAMES, TASK_QUEUE_NAME } from '@/modules/task-queue/task-queue.constants'
1400
+ import { ENV_ARR } from '@/utils/env-arr'
1401
+
1402
+ const describeQueueIntegration =
1403
+ process.env.RUN_QUEUE_INTEGRATION === 'true' ? describe : describe.skip
1404
+
1405
+ describeQueueIntegration('TaskQueue Redis integration', () => {
1406
+ let testingModule: TestingModule | undefined
1407
+ let queue: Queue | undefined
1408
+ let queueEvents: QueueEvents | undefined
1409
+ let taskQueueService: TaskQueueService | undefined
1410
+
1411
+ const previousPrefix = process.env.QUEUE_PREFIX
1412
+ const previousWorkerEnabled = process.env.QUEUE_WORKER_ENABLED
1413
+ const uniquePrefix = 'bubbles:test:' + randomUUID() + ':queue'
1414
+
1415
+ beforeAll(async () => {
1416
+ if (process.env.NODE_ENV === 'production') {
1417
+ throw new Error('Queue integration test must not run in production')
1418
+ }
1419
+
1420
+ process.env.QUEUE_PREFIX = uniquePrefix
1421
+ process.env.QUEUE_WORKER_ENABLED = 'true'
1422
+
1423
+ testingModule = await Test.createTestingModule({
1424
+ imports: [
1425
+ ConfigModule.forRoot({
1426
+ isGlobal: true,
1427
+ envFilePath: ENV_ARR,
1428
+ load: [queueConfig],
1429
+ }),
1430
+ TaskQueueModule,
1431
+ ],
1432
+ }).compile()
1433
+
1434
+ // 必须调用。compile() 本身不会触发
1435
+ // QueueWorkerBootstrap.onApplicationBootstrap()
1436
+ await testingModule.init()
1437
+
1438
+ queue = testingModule.get<Queue>(getQueueToken(TASK_QUEUE_NAME))
1439
+ taskQueueService = testingModule.get(TaskQueueService)
1440
+
1441
+ // QueueEvents 必须与 Queue 使用完全相同的
1442
+ // connection、DB 和 prefix。
1443
+ queueEvents = new QueueEvents(TASK_QUEUE_NAME, {
1444
+ connection: queue.opts.connection,
1445
+ prefix: queue.opts.prefix,
1446
+ })
1447
+
1448
+ // 必须先 ready,再投递,避免错过完成事件。
1449
+ await queueEvents.waitUntilReady()
1450
+ })
1451
+
1452
+ it('completes a Job through real Redis', async () => {
1453
+ if (!queue || !queueEvents || !taskQueueService) {
1454
+ throw new Error('Queue integration test was not initialized')
1455
+ }
1456
+
1457
+ const queued = await taskQueueService.enqueue(
1458
+ TASK_JOB_NAMES.QUEUE_SMOKE,
1459
+ {
1460
+ marker: 'redis-smoke',
1461
+ enqueuedAt: new Date().toISOString(),
1462
+ },
1463
+ {
1464
+ jobId: 'queue-smoke-' + randomUUID(),
1465
+ },
1466
+ )
1467
+
1468
+ const job = await queue.getJob(queued.jobId)
1469
+
1470
+ if (!job) {
1471
+ throw new Error('Smoke Job was not found')
1472
+ }
1473
+
1474
+ const result = await job.waitUntilFinished(queueEvents, 10_000)
1475
+
1476
+ expect(result.marker).toBe('redis-smoke')
1477
+ }, 15_000)
1478
+
1479
+ afterAll(async () => {
1480
+ try {
1481
+ await queueEvents?.close()
1482
+
1483
+ if (queue) {
1484
+ if (queue.opts.prefix !== uniquePrefix) {
1485
+ throw new Error('Refusing to clean a non-test Queue prefix')
1486
+ }
1487
+
1488
+ // 这里只清理由随机 Prefix 创建的测试队列。
1489
+ await queue.obliterate({ force: true })
1490
+ }
1491
+ } finally {
1492
+ try {
1493
+ await testingModule?.close()
1494
+ } finally {
1495
+ if (previousPrefix === undefined) {
1496
+ delete process.env.QUEUE_PREFIX
1497
+ } else {
1498
+ process.env.QUEUE_PREFIX = previousPrefix
1499
+ }
1500
+
1501
+ if (previousWorkerEnabled === undefined) {
1502
+ delete process.env.QUEUE_WORKER_ENABLED
1503
+ } else {
1504
+ process.env.QUEUE_WORKER_ENABLED = previousWorkerEnabled
1505
+ }
1506
+ }
1507
+ }
1508
+ })
1509
+ })
1510
+ ```
1511
+
1512
+ 这里的 obliterate 受到随机测试 Prefix 的显式保护,只清理本测试创建的键。不得删除 Prefix 检查,也不得把这段清理代码用于开发或生产队列。
1513
+
1514
+ 如果不关闭 QueueEvents 和 TestingModule,Worker 的阻塞连接会让测试进程无法退出。
1515
+
1516
+ 不要只导入裸 ConfigModule。否则 queueConfig 不会注册,TaskQueueModule 中的 getOrThrow('queue') 会失败。
1517
+
1518
+ 这个测试必须连接本地或专用测试 Redis,不能加载生产 Redis 凭据。唯一 Prefix 只能防止键名冲突,不能把生产 Redis 自动变成测试环境。
1519
+
1520
+ 不要把 ioredis-mock 当作最终验收。BullMQ 依赖 Lua、Streams 和阻塞命令,模拟 Redis 通常不完整。
1521
+
1522
+ ### 手工验证
1523
+
1524
+ ```powershell
1525
+ docker compose up -d redis
1526
+ $env:RUN_QUEUE_INTEGRATION = 'true'
1527
+ try {
1528
+ vp run --filter server test
1529
+ } finally {
1530
+ Remove-Item Env:RUN_QUEUE_INTEGRATION -ErrorAction SilentlyContinue
1531
+ }
1532
+ vp run --filter server build
1533
+ ```
1534
+
1535
+ 未显式设置 RUN_QUEUE_INTEGRATION 时,普通 `vp run --filter server test` 和 `vp run ready` 会跳过这一个真实 Redis 测试,但仍会执行其他单元测试。
1536
+
1537
+ 如需查看键,使用 SCAN,不要使用 KEYS:
1538
+
1539
+ ```powershell
1540
+ $queueRedisPassword = Read-Host '请输入本地 Redis 密码'
1541
+ docker compose exec -e REDISCLI_AUTH=$queueRedisPassword redis redis-cli -n 1 --scan --pattern 'bubbles:development:queue:server-tasks:*'
1542
+ Remove-Variable queueRedisPassword
1543
+ ```
1544
+
1545
+ ---
1546
+
1547
+ ## 19. 第 14 步:以后新业务任务怎么接入
1548
+
1549
+ 每增加一种任务,都按固定顺序操作。
1550
+
1551
+ ### 19.1 增加任务名
1552
+
1553
+ ```ts
1554
+ export const TASK_JOB_NAMES = {
1555
+ QUEUE_SMOKE: 'system.queue-smoke.v1',
1556
+ SOME_FUTURE_TASK: 'some-domain.some-action.v1',
1557
+ } as const
1558
+ ```
1559
+
1560
+ 不要直接复制这个占位任务名到生产代码,应换成真实领域和动作。
1561
+
1562
+ ### 19.2 增加 Zod Schema
1563
+
1564
+ ```ts
1565
+ export const SomeFutureTaskPayloadSchema = z.object({
1566
+ resourceId: z.string().uuid(),
1567
+ requestedBy: z.string().uuid(),
1568
+ })
1569
+ ```
1570
+
1571
+ ### 19.3 加入 TaskPayloadMap 和 Schema Map
1572
+
1573
+ ```ts
1574
+ export interface TaskPayloadMap {
1575
+ [TASK_JOB_NAMES.QUEUE_SMOKE]: z.infer<typeof QueueSmokePayloadSchema>
1576
+ [TASK_JOB_NAMES.SOME_FUTURE_TASK]: z.infer<typeof SomeFutureTaskPayloadSchema>
1577
+ }
1578
+
1579
+ export const TASK_PAYLOAD_SCHEMAS = {
1580
+ [TASK_JOB_NAMES.QUEUE_SMOKE]: QueueSmokePayloadSchema,
1581
+ [TASK_JOB_NAMES.SOME_FUTURE_TASK]: SomeFutureTaskPayloadSchema,
1582
+ } satisfies Record<TaskName, z.ZodType>
1583
+ ```
1584
+
1585
+ ### 19.4 业务模块导入 TaskQueueModule
1586
+
1587
+ ```ts
1588
+ @Module({
1589
+ imports: [TaskQueueModule],
1590
+ providers: [SomeBusinessService],
1591
+ })
1592
+ export class SomeBusinessModule {}
1593
+ ```
1594
+
1595
+ ### 19.5 业务 Service 只负责投递
1596
+
1597
+ ```ts
1598
+ constructor(
1599
+ private readonly taskQueue: TaskQueueService,
1600
+ ) {}
1601
+
1602
+ async startFutureTask(
1603
+ resourceId: string,
1604
+ userId: string,
1605
+ ) {
1606
+ return this.taskQueue.enqueue(
1607
+ TASK_JOB_NAMES.SOME_FUTURE_TASK,
1608
+ {
1609
+ resourceId,
1610
+ requestedBy: userId,
1611
+ },
1612
+ {
1613
+ jobId: 'some-future-task-' + resourceId,
1614
+ },
1615
+ )
1616
+ }
1617
+ ```
1618
+
1619
+ ### 19.6 在唯一 Processor 中增加分发
1620
+
1621
+ 第一版可在 switch 中增加分支,并把真实逻辑委托给单独的 Handler Service。
1622
+
1623
+ ```ts
1624
+ case TASK_JOB_NAMES.SOME_FUTURE_TASK: {
1625
+ const parsed = SomeFutureTaskPayloadSchema.safeParse(
1626
+ job.data,
1627
+ )
1628
+
1629
+ if (!parsed.success) {
1630
+ throw new UnrecoverableError(
1631
+ 'Invalid payload for ' + job.name,
1632
+ )
1633
+ }
1634
+
1635
+ return this.someFutureTaskHandler.handle(parsed.data)
1636
+ }
1637
+ ```
1638
+
1639
+ 同时从 contracts 文件导入 SomeFutureTaskPayloadSchema;UnrecoverableError 已在公共 Processor 中导入。不要把未经校验的 job.data 直接传给 Handler。
1640
+
1641
+ Processor 负责:
1642
+
1643
+ - 路由
1644
+ - Payload 运行时校验
1645
+ - 公共日志
1646
+ - 决定永久错误还是可重试错误
1647
+
1648
+ Handler 负责:
1649
+
1650
+ - 读取最新数据库状态
1651
+ - 幂等判断
1652
+ - 执行真实业务
1653
+ - 保存结果
1654
+
1655
+ ### 19.7 为任务补测试
1656
+
1657
+ 每个真实任务至少测试:
1658
+
1659
+ - 首次执行成功
1660
+ - 临时失败会重试
1661
+ - 永久错误不重试
1662
+ - 同一任务重复执行仍安全
1663
+ - 已完成状态重复执行会幂等返回
1664
+ - 日志不泄露敏感 Payload
1665
+
1666
+ ---
1667
+
1668
+ ## 20. 生产环境必须知道的边界
1669
+
1670
+ ### 20.1 数据库与 Redis 不是一个事务
1671
+
1672
+ 以下风险真实存在:
1673
+
1674
+ ```text
1675
+ PostgreSQL 提交成功
1676
+ ↓
1677
+ 进程在 queue.add 前崩溃
1678
+ ↓
1679
+ 数据库有记录,但任务没有进入 Redis
1680
+ ```
1681
+
1682
+ 第一版基础队列先接受这个边界。
1683
+
1684
+ 以后某类任务如果“绝对不能漏”,应使用 Transactional Outbox:
1685
+
1686
+ ```text
1687
+ 同一个数据库事务
1688
+ ├── 写业务数据
1689
+ └── 写 outbox 事件
1690
+
1691
+ 独立发布器
1692
+ └── 把 outbox 可靠投递到 BullMQ
1693
+ ```
1694
+
1695
+ 不要把当前方案描述成恰好一次或强一致。
1696
+
1697
+ ### 20.2 Redis 内存策略
1698
+
1699
+ BullMQ 使用的 Redis 必须采用:
1700
+
1701
+ ```text
1702
+ maxmemory-policy noeviction
1703
+ ```
1704
+
1705
+ 当前 Redis 默认策略通常就是 noeviction,但生产部署必须显式检查:
1706
+
1707
+ ```text
1708
+ CONFIG GET maxmemory-policy
1709
+ ```
1710
+
1711
+ 如果 Redis 在内存紧张时自动淘汰 BullMQ 键,队列状态可能损坏。
1712
+
1713
+ ### 20.3 持久化
1714
+
1715
+ 当前 Docker Redis 已开启 AOF,这是正确基础。生产环境还要考虑:
1716
+
1717
+ - AOF 策略
1718
+ - 磁盘容量
1719
+ - 备份
1720
+ - 故障恢复演练
1721
+ - Redis 高可用
1722
+
1723
+ ### 20.4 API 与 Worker 同进程
1724
+
1725
+ 第一版同进程是合理的,部署简单。
1726
+
1727
+ 出现以下情况时再拆:
1728
+
1729
+ - Worker 抢占 API CPU 或内存。
1730
+ - API 和 Worker 需要不同副本数。
1731
+ - Job 执行时间很长。
1732
+ - 需要 API 快速失败、Worker 持续重连的独立 Redis 策略。
1733
+ - 需要独立发布和故障隔离。
1734
+
1735
+ 拆分后 TaskQueueService 的业务调用方式不应变化。
1736
+
1737
+ ### 20.5 CPU 密集任务
1738
+
1739
+ CPU 密集任务会阻塞 Node.js 事件循环,Worker 可能无法及时续锁,导致 Job 被判定 stalled 并重复执行。
1740
+
1741
+ 这类任务应使用:
1742
+
1743
+ - 独立 Worker 进程
1744
+ - BullMQ 沙箱处理器
1745
+ - Worker Thread
1746
+ - 专门的计算服务
1747
+
1748
+ ### 20.6 监控
1749
+
1750
+ 第一版至少记录:
1751
+
1752
+ - Job completed
1753
+ - Job failed
1754
+ - Worker error
1755
+ - jobId
1756
+ - jobName
1757
+ - attemptsMade
1758
+
1759
+ 后续生产监控应增加:
1760
+
1761
+ - waiting 数量
1762
+ - active 数量
1763
+ - failed 数量
1764
+ - stalled 次数
1765
+ - 最老等待任务时长
1766
+ - 单任务耗时分布
1767
+
1768
+ Bull Board 可以以后作为受保护的内部管理页面接入,本次不做。
1769
+
1770
+ ---
1771
+
1772
+ ## 21. 常见故障排查
1773
+
1774
+ | 现象 | 常见原因 | 处理方向 |
1775
+ | -------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
1776
+ | 投递成功但没有消费 | TaskQueueModule 未导入、Worker 开关关闭或 Processor 未注册 | 检查 AppModule、QUEUE_WORKER_ENABLED 和启动日志 |
1777
+ | 任务失败却显示 completed | Worker catch 后返回了错误对象 | 让异常继续抛出 |
1778
+ | 同一任务执行两次 | 至少一次语义,处理器缺少幂等 | 增加数据库唯一约束或状态条件 |
1779
+ | 未知任务不断重试 | 未使用 UnrecoverableError | 把未知协议视为永久错误 |
1780
+ | Redis 内存持续增长 | 未配置保留上限或 Payload 太大 | 检查 removeOnComplete、removeOnFail 和 Payload |
1781
+ | Session 键和队列键混在一起 | DB 或 Prefix 配错 | 本地 Session DB 0、Queue DB 1 |
1782
+ | 测试执行完不退出 | QueueEvents 或 TestingModule 未关闭 | 在 afterAll 关闭资源 |
1783
+ | Redis Cluster 报 DB 错误 | Cluster 只能使用 DB 0 | 改为 DB 0 和独立 Prefix |
1784
+ | 自定义 jobId 报错 | jobId 含冒号 | 改用连字符 |
1785
+ | BullMQ 键结构异常 | 使用了 ioredis keyPrefix | 删除 keyPrefix,使用 BullMQ prefix |
1786
+ | Worker 出现重试配置警告 | 共享配置显式设置 maxRetriesPerRequest: 1 | 第一版共享配置中省略该项 |
1787
+ | HTTP 一直等业务结果 | 把入队完成误解为执行完成 | HTTP 返回 202 和 jobId |
1788
+ | Job 查不到 | 完成保留策略太短,或测试 Prefix 不一致 | 检查保留参数和 Prefix |
1789
+
1790
+ ---
1791
+
1792
+ ## 22. 最终验收顺序
1793
+
1794
+ 按下面顺序执行,某一步失败就先停下来修正:
1795
+
1796
+ ```powershell
1797
+ docker compose up -d redis
1798
+ vp install
1799
+ vp check
1800
+ vp run --filter server test
1801
+ $env:RUN_QUEUE_INTEGRATION = 'true'
1802
+ try {
1803
+ vp run --filter server test
1804
+ } finally {
1805
+ Remove-Item Env:RUN_QUEUE_INTEGRATION -ErrorAction SilentlyContinue
1806
+ }
1807
+ vp run --filter server build
1808
+ vp run ready
1809
+ ```
1810
+
1811
+ 第一次 `vp run --filter server test` 验证普通测试,并确认真实 Redis 测试默认跳过;设置 RUN_QUEUE_INTEGRATION 后的第二次测试才验证真实 Redis 链路。finally 会清理环境开关,确保后面的 `vp run ready` 恢复默认行为。
1812
+
1813
+ ### 历史实施验收清单
1814
+
1815
+ 以下复选框保留原实施方案的验收范围,不表示当前代码尚未完成;当前结论应以专项测试和交付记录为准。
1816
+
1817
+ - [ ] 使用 @nestjs/bullmq,不是 @nestjs/bull。
1818
+ - [ ] @nestjs/bullmq 和 bullmq 已加入 workspace catalog。
1819
+ - [ ] Session Redis DB 0,开发队列 DB 1。
1820
+ - [ ] 没有复用 @nestjs-modules/ioredis 创建的 Session Redis Client。
1821
+ - [ ] 没有复制 commandTimeout 和 enableOfflineQueue 到 BullMQ。
1822
+ - [ ] 共享连接没有显式设置 maxRetriesPerRequest: 1。
1823
+ - [ ] 队列名和任务名集中定义。
1824
+ - [ ] 业务模块只调用 TaskQueueService。
1825
+ - [ ] Payload 同时有 TypeScript 类型和 Zod 校验。
1826
+ - [ ] 默认重试、指数退避和保留上限已生效。
1827
+ - [ ] 每个进程对一个物理队列只注册一种能识别全部任务名的公共 WorkerHost。
1828
+ - [ ] 未知任务名和非法 Payload 不会无意义重试。
1829
+ - [ ] Worker 不吞异常。
1830
+ - [ ] Worker 日志不打印完整 Payload。
1831
+ - [ ] QUEUE_WORKER_ENABLED=false 时仍可投递,但当前进程不消费。
1832
+ - [ ] app.enableShutdownHooks() 已开启。
1833
+ - [ ] 单元测试通过。
1834
+ - [ ] 真实 Redis 完成投递、消费和完成全链路。
1835
+ - [ ] vp run ready 通过。
1836
+ - [ ] 没有实现任何真实业务任务。
1837
+ - [ ] 上传清理仅存在于附加方案文档,没有实现业务代码。
1838
+
1839
+ ---
1840
+
1841
+ ## 附录 A:未来上传清理如何接入
1842
+
1843
+ 详细方案已经拆分到独立文件:[上传清理设计方案](../../02.上传与文件/上传清理/设计方案.md)。该文件说明如何复用公共队列,但相关清理业务尚未进入当前源码。
1844
+
1845
+ 以后可能增加:
1846
+
1847
+ ```text
1848
+ upload.multipart-expire.v1
1849
+ upload.session-reconcile.v1
1850
+ ```
1851
+
1852
+ Payload 只传上传会话 ID:
1853
+
1854
+ ```json
1855
+ {
1856
+ "uploadSessionId": "..."
1857
+ }
1858
+ ```
1859
+
1860
+ 未来 Handler 应遵守:
1861
+
1862
+ 1. 根据 uploadSessionId 重新读取数据库。
1863
+ 2. 校验会话当前状态和过期时间。
1864
+ 3. 通过状态条件更新或租约领取处理权。
1865
+ 4. 已完成、已取消或已过期时幂等返回。
1866
+ 5. 不相信 Payload 里的 Bucket、Object Key 或 MinIO UploadId。
1867
+ 6. 从数据库读取真实存储信息。
1868
+ 7. 对象存储操作成功后再更新最终状态。
1869
+ 8. 能安全应对同一 Job 重复执行。
1870
+
1871
+ 要自动周期性扫描过期上传时,还需要另外选择调度方式,例如 NestJS Schedule 或 BullMQ Job Scheduler。本次不要提前安装调度依赖,也不要实现上传清理。
1872
+
1873
+ ---
1874
+
1875
+ ## 附录 B:本次建议的实施边界
1876
+
1877
+ 本轮真正执行时,只做:
1878
+
1879
+ ```text
1880
+ 公共配置
1881
+ 公共投递
1882
+ 公共消费
1883
+ 冒烟验证
1884
+ ```
1885
+
1886
+ 不要顺手加入:
1887
+
1888
+ ```text
1889
+ 上传清理
1890
+ 任务管理后台
1891
+ 任务进度接口
1892
+ 定时扫描
1893
+ Outbox
1894
+ 邮件或通知业务
1895
+ ```
1896
+
1897
+ 先让公共队列稳定跑通,再按真实业务逐个接任务,后端结构会更清楚,也更容易定位问题。