create-bubbles 0.1.25 → 0.1.26

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 (203) 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/.spaces/01./344/274/201/344/270/232/347/272/247/351/241/271/347/233/256/347/272/247//351/234/200/346/261/202/346/226/207/346/241/243.md +177 -0
  41. package/template-vp-monorepo-react-nestjs/.spaces/server/04.upload.md +4504 -0
  42. package/template-vp-monorepo-react-nestjs/.spaces/server/05./344/270/212/344/274/240/346/224/271/351/200/240/346/226/271/346/241/210/README.md +858 -0
  43. package/template-vp-monorepo-react-nestjs/.spaces/server/06./345/205/254/345/205/261/351/230/237/345/210/227/346/216/245/345/205/245/346/226/271/346/241/210/README.md +1890 -0
  44. package/template-vp-monorepo-react-nestjs/.spaces/server/06./345/205/254/345/205/261/351/230/237/345/210/227/346/216/245/345/205/245/346/226/271/346/241/210//344/270/212/344/274/240/346/270/205/347/220/206.md +1465 -0
  45. package/template-vp-monorepo-react-nestjs/AGENTS.md +8 -12
  46. package/template-vp-monorepo-react-nestjs/apps/server/.agents/rules/error-handling.md +84 -0
  47. package/template-vp-monorepo-react-nestjs/apps/server/.env.development +60 -0
  48. package/template-vp-monorepo-react-nestjs/apps/server/.env.production +16 -0
  49. package/template-vp-monorepo-react-nestjs/apps/server/AGENTS.md +9 -0
  50. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0000_military_colonel_america.sql +11 -0
  51. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/0001_loud_siren.sql +27 -0
  52. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0000_snapshot.json +97 -0
  53. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/0001_snapshot.json +354 -0
  54. package/template-vp-monorepo-react-nestjs/apps/server/drizzle/meta/_journal.json +20 -0
  55. package/template-vp-monorepo-react-nestjs/apps/server/package.json +4 -0
  56. package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +14 -2
  57. package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +6 -0
  58. package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +2 -0
  59. package/template-vp-monorepo-react-nestjs/apps/server/src/config/queue.config.ts +105 -0
  60. package/template-vp-monorepo-react-nestjs/apps/server/src/config/storage.config.ts +33 -0
  61. package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +57 -1
  62. package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +1 -0
  63. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +1 -3
  64. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +2 -2
  65. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/queue-worker.bootstrap.ts +24 -0
  66. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.constants.ts +24 -0
  67. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.contracts.ts +35 -0
  68. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.module.ts +56 -0
  69. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.processor.ts +62 -0
  70. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/task-queue/task-queue.service.ts +58 -0
  71. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/error-catelog.spec.ts +34 -0
  72. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/initiate-mutipart-upload.dto.ts +26 -0
  73. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/dto/upload-params.dto.ts +15 -0
  74. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/exact-size.transform.ts +66 -0
  75. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/minio-storage.adapter.ts +239 -0
  76. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/storage/storage.port.ts +61 -0
  77. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.constants.ts +23 -0
  78. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.controller.ts +109 -0
  79. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.errors.ts +80 -0
  80. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.module.ts +20 -0
  81. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.reponsitory.ts +166 -0
  82. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/upload.service.ts +696 -0
  83. package/template-vp-monorepo-react-nestjs/apps/server/src/modules/upload/utils/object-key.ts +10 -0
  84. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.integration.spec.ts +120 -0
  85. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.processor.spec.ts +63 -0
  86. package/template-vp-monorepo-react-nestjs/apps/server/test/task-queue.service.spec.ts +106 -0
  87. package/template-vp-monorepo-react-nestjs/apps/web/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. package/template-vp-monorepo-react-nestjs/apps/web/.env.dev +1 -1
  94. package/template-vp-monorepo-react-nestjs/apps/web/AGENTS.md +16 -0
  95. package/template-vp-monorepo-react-nestjs/apps/web/README.MD +153 -0
  96. package/template-vp-monorepo-react-nestjs/apps/web/index.html +3 -2
  97. package/template-vp-monorepo-react-nestjs/apps/web/package.json +6 -1
  98. package/template-vp-monorepo-react-nestjs/apps/web/public/favicon.svg +9 -1
  99. package/template-vp-monorepo-react-nestjs/apps/web/src/App.module.css +5 -0
  100. package/template-vp-monorepo-react-nestjs/apps/web/src/App.tsx +16 -9
  101. package/template-vp-monorepo-react-nestjs/apps/web/src/api/auth.ts +9 -0
  102. package/template-vp-monorepo-react-nestjs/apps/web/src/assets/svg/draft.svg +4 -0
  103. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.module.css +78 -0
  104. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/DraftProTable.tsx +99 -0
  105. package/template-vp-monorepo-react-nestjs/apps/web/src/components/DraftProTable/README.md +34 -0
  106. package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.module.css +54 -0
  107. package/template-vp-monorepo-react-nestjs/apps/web/src/components/FullHeightProTable/FullHeightProTable.tsx +41 -0
  108. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/README.md +37 -0
  109. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  110. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.module.css +7 -0
  111. package/template-vp-monorepo-react-nestjs/apps/web/src/components/Loading/PageLoading.tsx +2 -1
  112. package/template-vp-monorepo-react-nestjs/apps/web/src/config/theme.ts +20 -0
  113. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/BasicLayout.module.css +17 -0
  114. package/template-vp-monorepo-react-nestjs/apps/web/src/layouts/BasicLayout.tsx +106 -0
  115. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.module.css +23 -0
  116. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/i18n/index.tsx +53 -0
  117. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/components/ProjectFormDialog.tsx +132 -0
  118. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/columns.tsx +121 -0
  119. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/config/index.ts +77 -0
  120. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/components/DraftProjectFormDialog.tsx +140 -0
  121. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/columns.tsx +127 -0
  122. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/index.ts +39 -0
  123. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/config/projects.ts +48 -0
  124. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.module.css +49 -0
  125. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/draft/index.tsx +163 -0
  126. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.module.css +53 -0
  127. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/examples/pro-table/index.tsx +135 -0
  128. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.module.css +23 -0
  129. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/home/index.tsx +26 -43
  130. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/api.ts +11 -0
  131. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/index.tsx +183 -0
  132. package/template-vp-monorepo-react-nestjs/apps/web/src/pages/login/login.css +164 -0
  133. package/template-vp-monorepo-react-nestjs/apps/web/src/router/index.tsx +36 -12
  134. package/template-vp-monorepo-react-nestjs/apps/web/src/styles/index.css +11 -4
  135. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/core/index.ts +55 -47
  136. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/request/index.ts +10 -5
  137. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/cookie.ts +50 -0
  138. package/template-vp-monorepo-react-nestjs/apps/web/src/utils/storage/session.ts +33 -0
  139. package/template-vp-monorepo-react-nestjs/apps/web/test/draft-projects.spec.ts +89 -0
  140. package/template-vp-monorepo-react-nestjs/apps/web/test/request.spec.ts +145 -0
  141. package/template-vp-monorepo-react-nestjs/apps/web/test/storage.spec.ts +108 -0
  142. package/template-vp-monorepo-react-nestjs/apps/web/test/tsconfig.json +7 -0
  143. package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +1 -1
  144. package/template-vp-monorepo-react-nestjs/apps/web/vite.config.ts +7 -0
  145. package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.tsbuildinfo +1 -0
  146. package/template-vp-monorepo-react-nestjs/docker-compose.yml +19 -0
  147. package/template-vp-monorepo-react-nestjs/mise.toml +1 -1
  148. package/template-vp-monorepo-react-nestjs/package.json +1 -6
  149. package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +9 -1
  150. package/template-vp-react/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  151. package/template-vp-react/AGENTS.md +11 -0
  152. package/template-vp-react/commitlint.config.js +0 -0
  153. package/template-vp-react/package.json +21 -26
  154. package/template-vp-react/pnpm-workspace.yaml +6 -0
  155. package/template-vp-react/src/assets/svg/draft.svg +4 -0
  156. package/template-vp-react/src/components/Icon/svg-icon/README.md +37 -0
  157. package/template-vp-react/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  158. package/template-vp-react/src/utils/request/index.ts +18 -5
  159. package/template-vp-react/tsconfig.json +2 -3
  160. package/template-vp-react/vite.config.ts +2 -0
  161. package/template-vp-react-shadcn/.agents/rules/SVG/344/275/277/347/224/250.md +8 -0
  162. package/template-vp-react-shadcn/.vscode/settings.json +2 -2
  163. package/template-vp-react-shadcn/{agents.md → AGENTS.md} +12 -2
  164. package/template-vp-react-shadcn/README.md +41 -0
  165. package/template-vp-react-shadcn/commitlint.config.js +1 -1
  166. package/template-vp-react-shadcn/components.json +1 -1
  167. package/template-vp-react-shadcn/package.json +25 -32
  168. package/template-vp-react-shadcn/pnpm-workspace.yaml +10 -0
  169. package/template-vp-react-shadcn/src/App.tsx +10 -10
  170. package/template-vp-react-shadcn/src/assets/svg/draft.svg +4 -0
  171. package/template-vp-react-shadcn/src/components/Icon/svg-icon/README.md +37 -0
  172. package/template-vp-react-shadcn/src/components/Icon/svg-icon/SvgIcon.tsx +27 -0
  173. package/template-vp-react-shadcn/src/components/Loading/PageLoading.tsx +4 -4
  174. package/template-vp-react-shadcn/src/components/Player/VideoJS/index.tsx +94 -78
  175. package/template-vp-react-shadcn/src/components/Player/index.ts +1 -1
  176. package/template-vp-react-shadcn/src/components/ui/button.tsx +25 -32
  177. package/template-vp-react-shadcn/src/components/ui/spinner.tsx +8 -7
  178. package/template-vp-react-shadcn/src/components/ui/toast.tsx +229 -0
  179. package/template-vp-react-shadcn/src/lib/utils.ts +3 -3
  180. package/template-vp-react-shadcn/src/main.tsx +5 -4
  181. package/template-vp-react-shadcn/src/pages/home/index.tsx +43 -35
  182. package/template-vp-react-shadcn/src/router/index.tsx +15 -15
  183. package/template-vp-react-shadcn/src/styles/index.css +4 -4
  184. package/template-vp-react-shadcn/src/styles/shadcn.css +2 -2
  185. package/template-vp-react-shadcn/src/utils/env/index.tsx +2 -2
  186. package/template-vp-react-shadcn/src/utils/request/alova-core/index.ts +102 -30
  187. package/template-vp-react-shadcn/src/utils/request/alova-core/utils.ts +5 -2
  188. package/template-vp-react-shadcn/src/utils/request/index.ts +32 -19
  189. package/template-vp-react-shadcn/src/utils/request/readme.md +41 -38
  190. package/template-vp-react-shadcn/tsconfig.json +2 -3
  191. package/template-vp-react-shadcn/vite.config.ts +29 -15
  192. package/template-vp-vue-eslint-vapor/package.json +26 -33
  193. package/template-vp-vue-eslint-vapor/pnpm-workspace.yaml +29 -1
  194. package/template-vue-vp-eslint/README.md +3 -10
  195. package/template-vue-vp-eslint/package.json +27 -34
  196. package/template-vue-vp-eslint/pnpm-workspace.yaml +27 -2
  197. package/template-vue-vp-eslint/src/layout/default/index.vue +4 -3
  198. package/template-vp-monorepo-react-nestjs/.agents/auth.config.ts +0 -0
  199. package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +0 -991
  200. package/template-vp-monorepo-react-nestjs/apps/server/README.md +0 -3
  201. package/template-vp-react-shadcn/.oxfmtrc.jsonc +0 -8
  202. package/template-vp-react-shadcn/.oxlintrc.jsonc +0 -3
  203. package/template-vp-react-shadcn/src/components/ui/sonner.tsx +0 -43
@@ -0,0 +1,858 @@
1
+ # 上传改造方案
2
+
3
+ > 状态:讨论稿,尚未实施
4
+ > 基线日期:2026-09-02
5
+ > 适用范围:apps/server、PostgreSQL、MinIO、Nginx
6
+ > 本文目标:把当前分片上传改造成“有长期文件记录、具备公司/项目归属、完成后返回标准文件信息、最终文件可经 Nginx 匿名只读访问”的完整方案。
7
+
8
+ ## 0. 结论先行
9
+
10
+ 本方案采用以下设计:
11
+
12
+ 1. 新增长期 files 表,记录每一个文件的业务归属和存储信息。
13
+ 2. upload_sessions 只负责分片上传过程,不再兼任长期文件表。
14
+ 3. 初始化上传时生成 fileId,并同时记录 companyId、projectId、uploadedBy。
15
+ 4. companyId、projectId 必须经过后端权限和归属校验,不能直接信任请求参数或 X-Company-Id。
16
+ 5. 完成上传后至少返回:
17
+ - fileId
18
+ - fileUrl
19
+ - fileName
20
+ - fileSuffix
21
+ - fileSize
22
+ - companyId
23
+ - projectId
24
+ 6. 完成响应不增加 uploadSize;上传进度接口如有需要,使用动态计算的 uploadedSize。
25
+ 7. 暂不增加 group。公司、项目、文件用途和可见性应使用含义明确的独立字段。
26
+ 8. fileUrl 返回相对地址,例如 /files/objects/2026/09/<fileId>,不返回 MinIO 内网地址。
27
+ 9. 最终公开文件放入独立 public-files Bucket,匿名权限仅允许 GetObject。
28
+ 10. Nginx 对外只开放 /files/\*\* 的 GET、HEAD;上传、完成、删除等接口继续由 NestJS 鉴权。
29
+ 11. MinIO 9000、9001 生产环境不直接暴露公网。
30
+
31
+ “不加鉴权”只表示最终文件下载不要求登录。它等价于公开文件:任何获得 URL 的人都可以访问,URL 难猜不能替代权限控制。
32
+
33
+ ## 1. 本次方案边界
34
+
35
+ ### 1.1 包含
36
+
37
+ - 永久文件记录与上传会话拆分。
38
+ - companyId、projectId 的传递、校验和持久化。
39
+ - 分片上传接口补齐及完成响应调整。
40
+ - fileId、fileUrl、文件名、后缀、大小等字段定义。
41
+ - MinIO 公共只读 Bucket。
42
+ - Nginx 文件代理。
43
+ - 数据迁移、幂等恢复、测试验收和回滚方案。
44
+
45
+ ### 1.2 暂不包含
46
+
47
+ - 本文档之外的实际代码修改。
48
+ - CDN 厂商接入。
49
+ - 病毒扫描、内容审核的具体实现。
50
+ - 私有文件下载授权实现。
51
+ - company、project、membership 业务模块的具体产品规则。
52
+
53
+ 如果以后需要私有文件,应保留 visibility 扩展能力,并使用短期预签名 URL、后端下载代理或专用签名网关,不能继续使用永久匿名 URL。
54
+
55
+ ## 2. 当前实现事实
56
+
57
+ 以下内容来自当前源码,不代表目标状态。
58
+
59
+ | 项目 | 当前状态 | 影响 |
60
+ | ------------- | --------------------------------------------------- | ------------------------------------------------ |
61
+ | 数据库 | schema.ts 中只有 upload_sessions,没有永久 files 表 | 上传完成后缺少稳定的文件业务实体 |
62
+ | 文件归属 | 只有 ownerId | 无法按公司、项目查询、授权和删除 |
63
+ | 完成响应 | 只有 uploadSessionId、status、objectKey、etag | 缺少前端需要的标准文件信息,并暴露内部 objectKey |
64
+ | 对象 Key | users/<ownerId>/<yyyy>/<MM>/<uploadSessionId> | 直接公开会暴露用户 UUID 和内部结构 |
65
+ | MinIO Bucket | uploads,anonymous policy 为 none | 普通 Nginx 匿名反代会得到 AccessDenied |
66
+ | MinIO 端口 | 9000、9001 映射到宿主机 | 生产环境不应直接暴露 |
67
+ | Nginx | 当前仓库没有文件代理配置 | /files/\*\* 尚不存在 |
68
+ | 公司/项目模型 | 当前 schema 中没有 company、project、membership 表 | 无法可靠校验 companyId、projectId |
69
+
70
+ 当前上传模块还存在以下接通问题,应在正式改造第一阶段处理:
71
+
72
+ - UploadController 目前只有初始化接口。
73
+ - UploadService.initiate() 没有进入真正的新建逻辑,实际逻辑位于误拼的 initate()。
74
+ - 当前没有 upload.module.ts,AppModule 也没有导入上传模块。
75
+ - UploadSessionParamsDto 错误使用了包含 partNumber 的参数 Schema。
76
+ - abort() 的部分成功路径没有返回统一响应。
77
+ - initiate-mutipart-upload.dto.ts、upload.reponsitory.ts 存在文件名拼写问题。
78
+ - 当前缺少上传模块专项测试。
79
+
80
+ 这些问题只在本文中登记,本次创建方案文档不会修改它们。
81
+
82
+ ## 3. 关键业务规则
83
+
84
+ ### 3.1 默认归属规则
85
+
86
+ 本方案默认:
87
+
88
+ - companyId 必填。
89
+ - projectId 必填。
90
+ - uploadedBy 从当前登录会话取得,客户端不能指定。
91
+ - 客户端在初始化上传时传 companyId、projectId。
92
+ - 后端必须查询项目并验证 project.companyId 与 companyId 一致。
93
+ - 后端必须验证当前用户属于该公司,并拥有对应项目的上传权限。
94
+
95
+ 如果后续确认存在“只属于公司、不属于项目”的文件,可将 projectId 改为可空;此时 null 必须明确表示“公司级文件”,不能表示未知归属。
96
+
97
+ ### 3.2 参数可信边界
98
+
99
+ - companyId、projectId、X-Company-Id 都属于客户端输入,不能直接作为可信事实入库。
100
+ - X-Company-Id 目前只被 CORS 放行,不等于后端已经建立租户上下文。
101
+ - 如果请求同时携带 X-Company-Id 和 companyId,两者必须一致。
102
+ - projectId 必须反查项目记录,不能仅验证 UUID 格式。
103
+ - 跨公司或跨项目访问建议统一返回 404,避免泄漏资源是否存在。
104
+
105
+ ### 3.3 操作权限建议
106
+
107
+ | 操作 | 默认权限 |
108
+ | ---------------- | --------------------------- |
109
+ | 初始化上传 | 公司成员且拥有项目上传权限 |
110
+ | 查询上传状态 | 原上传者;项目管理员可选 |
111
+ | 上传分片 | 原上传者 |
112
+ | 完成上传 | 原上传者 |
113
+ | 取消上传 | 原上传者;项目管理员可选 |
114
+ | 查询文件记录 | 按公司、项目业务权限 |
115
+ | 删除文件 | 项目管理员或明确授权角色 |
116
+ | 读取公开文件内容 | 无需登录,任何获得 URL 的人 |
117
+
118
+ ownerId 应重命名或在文件表中表达为 uploadedBy。它是审计字段,不应继续作为唯一租户授权边界。
119
+
120
+ ## 4. 目标架构
121
+
122
+ ```text
123
+ 控制面:必须鉴权
124
+
125
+ 浏览器 ── /api/uploads/multipart/** ──> Nginx ──> NestJS
126
+ │
127
+ ├── PostgreSQL
128
+ │ ├── files
129
+ │ └── upload_sessions
130
+ │
131
+ └── MinIO S3 API
132
+ └── public-files
133
+
134
+ 数据面:匿名只读
135
+
136
+ 浏览器 ── GET/HEAD /files/<objectKey> ──> Nginx ──> MinIO public-files
137
+ ```
138
+
139
+ ### 4.1 信任边界
140
+
141
+ - NestJS 使用 MinIO 凭据执行 CreateMultipartUpload、UploadPart、Complete、Abort、Delete。
142
+ - public-files 的匿名策略只允许读取对象,不允许列举和写入。
143
+ - Nginx 固定代理 public-files,不允许客户端选择 Bucket。
144
+ - 数据库保存业务归属;MinIO 只保存对象。
145
+ - fileUrl 是部署层访问地址,不是数据库事实。
146
+
147
+ ### 4.2 发布时点
148
+
149
+ 在简单公共 Bucket 方案中,MinIO CompleteMultipartUpload 成功的时刻就是对象公开的时刻。
150
+
151
+ 因此:
152
+
153
+ - 分片完整性、大小、类型等同步校验必须在 Complete 之前完成。
154
+ - 数据库在 Complete 后写入失败时,重试或后台对账必须恢复同一个 fileId,不能新建第二条文件记录。
155
+ - 由于对象 Key 不可推断且此时尚未向普通用户返回 fileUrl,短暂的数据库失败不会自动暴露 URL,但仍需孤儿对象对账。
156
+
157
+ 如果未来要求“病毒扫描通过后才公开”,应改为私有暂存区加发布流程。当前最大文件可达 90 GiB,大文件发布不能依赖单次 CopyObject,应设计 S3 Multipart Copy 或异步发布任务。
158
+
159
+ ## 5. 数据模型
160
+
161
+ ### 5.1 新增 files 表
162
+
163
+ 建议表名:files。
164
+
165
+ | 字段 | 类型建议 | 是否必填 | 说明 |
166
+ | ------------- | ------------- | -------- | ----------------------------------------------- |
167
+ | id | uuid | 是 | 对外 fileId,初始化上传时生成 |
168
+ | company_id | uuid | 是 | 文件所属公司 |
169
+ | project_id | uuid | 默认是 | 文件所属项目;只有明确支持公司级文件时才可空 |
170
+ | uploaded_by | uuid | 是 | 上传人,关联 users.id |
171
+ | bucket | varchar(63) | 是 | 后端内部字段 |
172
+ | object_key | varchar(1024) | 是 | 后端内部字段 |
173
+ | original_name | varchar(255) | 是 | 原始完整文件名 |
174
+ | file_suffix | varchar(32) | 否 | 小写且不包含点;无后缀为 null |
175
+ | content_type | varchar(255) | 是 | 服务端校验后的 MIME |
176
+ | file_size | bigint | 是 | 字节数,以最终对象校验结果为准 |
177
+ | object_etag | text | 否 | 存储对象 ETag,仅内部使用 |
178
+ | visibility | enum | 是 | 当前固定 public,预留 private |
179
+ | status | enum | 是 | uploading、available、failed、deleting、deleted |
180
+ | created_at | timestamptz | 是 | 创建时间 |
181
+ | completed_at | timestamptz | 否 | 上传完成时间 |
182
+ | deleted_at | timestamptz | 否 | 软删除时间 |
183
+ | updated_at | timestamptz | 是 | 更新时间 |
184
+
185
+ 建议约束与索引:
186
+
187
+ - 主键 files.id。
188
+ - 唯一索引 files(bucket, object_key)。
189
+ - 索引 files(company_id, project_id, status, created_at)。
190
+ - 索引 files(uploaded_by, created_at)。
191
+ - 如果 company、project 表已存在,建立外键。
192
+ - 如果同时保存 companyId、projectId,建议使用复合约束保证项目确实属于公司。
193
+
194
+ fileUrl 不写入数据库。它由公开前缀和 objectKey 在响应时生成,避免域名、Nginx、CDN 迁移导致历史数据失效。
195
+
196
+ ### 5.2 upload_sessions 调整
197
+
198
+ upload_sessions 保留现有分片技术状态,并新增:
199
+
200
+ | 字段 | 说明 |
201
+ | ------- | ------------------------------- |
202
+ | file_id | 关联 files.id,初始化后不可变化 |
203
+
204
+ 推荐最终职责:
205
+
206
+ - files:业务归属、最终文件、查询和删除生命周期。
207
+ - upload_sessions:storageUploadId、分片大小、分片数量、过期时间及上传状态。
208
+
209
+ 迁移初期可以暂时保留 upload_sessions 中重复的 bucket、objectKey、文件名、类型和大小,等新流程稳定后再决定是否清理。不要在第一版迁移中直接删除旧列。
210
+
211
+ ### 5.3 状态关系
212
+
213
+ ```text
214
+ files:
215
+
216
+ uploading ──完成成功──> available ──申请删除──> deleting ──物理删除──> deleted
217
+ │
218
+ ├──取消/过期──> failed
219
+ └──不可恢复错误──> failed
220
+
221
+ upload_sessions:
222
+
223
+ uploading ──抢占完成──> completing ──成功──> completed
224
+ │ │
225
+ ├──取消──> aborting ──> aborted
226
+ └──过期──> expired
227
+ ```
228
+
229
+ 上传会话状态与文件状态必须在同一业务流程中协调,但不能把两者混为一个字段。
230
+
231
+ ## 6. 文件标识、对象 Key 与 URL
232
+
233
+ ### 6.1 标识定义
234
+
235
+ - fileId:长期文件实体 ID。
236
+ - uploadSessionId:一次分片上传会话 ID。
237
+ - clientUploadId:客户端生成的幂等 ID。
238
+ - objectKey:MinIO 内部对象路径。
239
+ - fileUrl:通过 Nginx 公开访问对象的相对 URL。
240
+
241
+ fileId 与 uploadSessionId 不应继续混用。初始化时同时生成两者,后续完成重试始终使用同一个 fileId。
242
+
243
+ ### 6.2 对象 Key
244
+
245
+ 建议格式:
246
+
247
+ ```text
248
+ objects/<yyyy>/<MM>/<fileId>
249
+ ```
250
+
251
+ 示例:
252
+
253
+ ```text
254
+ objects/2026/09/1e2ab695-1827-4f88-87a2-c354cc5a6ed3
255
+ ```
256
+
257
+ 设计理由:
258
+
259
+ - 不包含 ownerId、companyId、projectId。
260
+ - 不包含原始文件名,避免路径注入和隐私泄漏。
261
+ - fileId 为服务端生成的高熵随机 UUID,不可顺序枚举。
262
+ - 文件名、公司和项目归属只保存在数据库。
263
+ - URL 是否带扩展名不影响 MinIO 读取,浏览器行为由 Content-Type 和 Content-Disposition 决定。
264
+
265
+ ### 6.3 fileUrl
266
+
267
+ ```text
268
+ /files/<objectKey>
269
+ ```
270
+
271
+ 示例:
272
+
273
+ ```text
274
+ /files/objects/2026/09/1e2ab695-1827-4f88-87a2-c354cc5a6ed3
275
+ ```
276
+
277
+ 数据库不保存 fileUrl。后端通过配置项 PUBLIC_FILE_URL_PREFIX 生成:
278
+
279
+ ```text
280
+ fileUrl = PUBLIC_FILE_URL_PREFIX + "/" + objectKey
281
+ ```
282
+
283
+ 默认 PUBLIC_FILE_URL_PREFIX 为 /files。未来切换到 files.example.com 或 CDN 时,只调整运行时配置或网关规则。
284
+
285
+ ### 6.4 对象响应元数据
286
+
287
+ 创建 Multipart Upload 时建议写入:
288
+
289
+ - Content-Type:服务端允许或校验后的类型。
290
+ - Content-Disposition:使用安全编码后的原始文件名。
291
+ - Cache-Control:根据撤回和缓存策略决定。
292
+
293
+ 不要把 ownerId、companyId、projectId 写入公共对象的自定义 x-amz-meta-\*。匿名 HEAD 请求可能看到这些元数据。
294
+
295
+ HTML、SVG 等可执行类型默认使用 attachment;只有经过明确白名单校验的图片、视频、PDF 等类型才考虑 inline。
296
+
297
+ ## 7. API 改造
298
+
299
+ 以下路径表示 NestJS 业务路由;部署时可由 Nginx 统一增加 /api 前缀。
300
+
301
+ ### 7.1 接口列表
302
+
303
+ | 方法 | 路径 | 鉴权 | 用途 |
304
+ | -------- | ----------------------------------------------------- | ---- | -------------------------------- |
305
+ | POST | /uploads/multipart | 是 | 初始化上传 |
306
+ | GET | /uploads/multipart/:uploadSessionId | 是 | 查询状态和已上传分片 |
307
+ | PUT | /uploads/multipart/:uploadSessionId/parts/:partNumber | 是 | 上传一个分片 |
308
+ | POST | /uploads/multipart/:uploadSessionId/complete | 是 | 完成上传并返回文件信息 |
309
+ | DELETE | /uploads/multipart/:uploadSessionId | 是 | 取消未完成上传 |
310
+ | DELETE | /files/:fileId | 是 | 删除已完成文件,后续文件模块实现 |
311
+ | GET/HEAD | /files/<objectKey> | 否 | Nginx 直达 MinIO 的公开文件内容 |
312
+
313
+ 业务 API 的 /api/files/:fileId 与公开内容路由 /files/<objectKey> 应由 Nginx 按前缀分流,不能落入同一个上游。
314
+
315
+ ### 7.2 初始化请求
316
+
317
+ ```json
318
+ {
319
+ "clientUploadId": "f42e5ed4-cb58-4ed0-a64a-16154454ae3e",
320
+ "companyId": "86d66115-9046-4a25-b869-09a29ab4f1f1",
321
+ "projectId": "bfb38c08-a1bd-48b5-92fb-39a4cf602952",
322
+ "fileName": "项目设计图.PDF",
323
+ "fileSize": 123456,
324
+ "contentType": "application/pdf"
325
+ }
326
+ ```
327
+
328
+ 初始化时后端必须:
329
+
330
+ 1. 校验公司、项目、成员关系和上传权限。
331
+ 2. 校验文件名、大小和 Content-Type。
332
+ 3. 从 fileName 提取 fileSuffix,转为小写且不含点。
333
+ 4. 生成 fileId、uploadSessionId、objectKey。
334
+ 5. 创建 MinIO multipart upload。
335
+ 6. 在数据库事务中创建 files 和 upload_sessions。
336
+ 7. 数据库失败时补偿 AbortMultipartUpload。
337
+
338
+ ### 7.3 初始化响应
339
+
340
+ ```json
341
+ {
342
+ "fileId": "1e2ab695-1827-4f88-87a2-c354cc5a6ed3",
343
+ "uploadSessionId": "b83915e5-091c-43cb-b43e-5d93e1624f8e",
344
+ "status": "uploading",
345
+ "partSize": 10485760,
346
+ "totalParts": 1,
347
+ "expiresAt": "2026-09-03T08:00:00.000Z",
348
+ "uploadedParts": []
349
+ }
350
+ ```
351
+
352
+ 初始化阶段不必返回 fileUrl,避免客户端把尚未完成的地址当作可用文件。
353
+
354
+ ### 7.4 状态响应
355
+
356
+ 状态接口保留 uploadedParts。若前端需要总上传进度,可增加:
357
+
358
+ ```json
359
+ {
360
+ "uploadedSize": 10485760
361
+ }
362
+ ```
363
+
364
+ uploadedSize 由已上传分片大小求和得到,不需要单独持久化。
365
+
366
+ ### 7.5 完成响应
367
+
368
+ ```json
369
+ {
370
+ "uploadSessionId": "b83915e5-091c-43cb-b43e-5d93e1624f8e",
371
+ "status": "completed",
372
+ "fileId": "1e2ab695-1827-4f88-87a2-c354cc5a6ed3",
373
+ "fileUrl": "/files/objects/2026/09/1e2ab695-1827-4f88-87a2-c354cc5a6ed3",
374
+ "fileName": "项目设计图.PDF",
375
+ "fileSuffix": "pdf",
376
+ "fileSize": 123456,
377
+ "contentType": "application/pdf",
378
+ "companyId": "86d66115-9046-4a25-b869-09a29ab4f1f1",
379
+ "projectId": "bfb38c08-a1bd-48b5-92fb-39a4cf602952"
380
+ }
381
+ ```
382
+
383
+ 字段规则:
384
+
385
+ | 字段 | 规则 |
386
+ | ----------- | ---------------------------------------------------- |
387
+ | fileId | 稳定的文件实体 ID,完成重试不能变化 |
388
+ | fileUrl | 相对地址,不包含 MinIO 域名、端口、Bucket 或签名参数 |
389
+ | fileName | 原始完整文件名 |
390
+ | fileSuffix | 小写、不含点;无扩展名返回 null |
391
+ | fileSize | 字节数,以已验证分片总和和 HeadObject 为准 |
392
+ | contentType | 服务端验证后的 MIME |
393
+ | companyId | 已通过权限验证的公司 |
394
+ | projectId | 已通过归属验证的项目 |
395
+
396
+ 不返回:
397
+
398
+ - storageUploadId。
399
+ - bucket。
400
+ - objectKey。
401
+ - MinIO endpoint。
402
+ - MinIO 凭据。
403
+ - uploadSize。
404
+ - 含义不明确的 group。
405
+
406
+ etag 是否返回由前端缓存需求决定。Multipart ETag 不能当作普通文件 MD5。
407
+
408
+ ### 7.6 幂等规则
409
+
410
+ - 同一 uploadedBy + clientUploadId 重试时返回同一个 fileId 和 uploadSessionId。
411
+ - 如果文件名、大小、类型、companyId 或 projectId 不一致,返回冲突错误。
412
+ - complete 重试必须返回同一份文件信息,不能重复插入 files。
413
+ - MinIO 已完成但数据库写入失败时,通过 HeadObject、会话中已持久化的 objectKey、fileId 和最终大小恢复同一记录,不依赖会泄漏业务信息的公共对象元数据。
414
+
415
+ ## 8. 上传与完成流程
416
+
417
+ ### 8.1 初始化
418
+
419
+ ```text
420
+ 校验身份和项目权限
421
+ → 生成 fileId、uploadSessionId、objectKey
422
+ → MinIO CreateMultipartUpload
423
+ → DB 事务创建 files(uploading) 和 upload_sessions(uploading)
424
+ → 返回初始化信息
425
+ ```
426
+
427
+ ### 8.2 分片上传
428
+
429
+ - 保留当前流式上传方式。
430
+ - 必须要求合法 Content-Length。
431
+ - 每片实际字节数必须与预期一致。
432
+ - 不允许 Content-Encoding 压缩改变字节长度。
433
+ - 每次查询、上传、完成、取消都必须重新校验会话归属。
434
+ - 多实例部署前需要解决“仍有分片在途时 complete/abort”的竞态;当前 PARTS_STILL_ACTIVE 已定义但未使用。
435
+
436
+ ### 8.3 完成
437
+
438
+ ```text
439
+ uploading
440
+ → 条件更新抢占为 completing
441
+ → ListParts
442
+ → 校验分片编号、大小、总字节数
443
+ → CompleteMultipartUpload
444
+ → HeadObject 校验最终大小和关键元数据
445
+ → DB 事务:
446
+ files.status = available
447
+ files.objectEtag = 最终 ETag
448
+ upload_sessions.status = completed
449
+ → 返回标准文件信息
450
+ ```
451
+
452
+ 在调用 MinIO Complete 之前失败,可以安全恢复为 uploading。
453
+
454
+ 调用 Complete 之后发生超时或未知错误时,不能直接恢复 uploading。应保持 completing,通过 HeadObject 判断对象是否已经存在,并执行同一套 finalize 事务。
455
+
456
+ ### 8.4 取消与过期
457
+
458
+ - uploading 或 expired 可以进入 aborting。
459
+ - AbortMultipartUpload 成功后,会话改为 aborted。
460
+ - 对应 files 状态改为 failed,供清理任务处理。
461
+ - completed 文件不能通过“取消上传”删除,必须走按 fileId 鉴权的文件删除接口。
462
+
463
+ ### 8.5 文件删除
464
+
465
+ 文件删除接口只接受 fileId,不接受客户端提供 bucket 或 objectKey。
466
+
467
+ 建议顺序:
468
+
469
+ 1. 校验公司、项目和删除权限。
470
+ 2. 条件更新 files.status 为 deleting。
471
+ 3. 按数据库中的 bucket、objectKey 删除对象。
472
+ 4. 更新为 deleted 并写 deletedAt。
473
+ 5. 如有 Nginx/CDN 缓存,执行 purge 或等待明确的短缓存过期。
474
+
475
+ 删除 MinIO 对象不代表已经被浏览器、代理或第三方保存的公开文件可以被完全收回。
476
+
477
+ ## 9. MinIO 公共只读方案
478
+
479
+ ### 9.1 Bucket
480
+
481
+ 推荐新增:
482
+
483
+ ```text
484
+ public-files
485
+ ```
486
+
487
+ 不要直接把未来可能存放私密内容的通用 uploads Bucket 整体公开。
488
+
489
+ public-files 的匿名策略只允许:
490
+
491
+ ```text
492
+ s3:GetObject
493
+ arn:aws:s3:::public-files/*
494
+ ```
495
+
496
+ 必须禁止:
497
+
498
+ - s3:ListBucket
499
+ - s3:PutObject
500
+ - s3:DeleteObject
501
+ - Multipart Upload API
502
+ - Bucket Policy、ACL 和管理 API
503
+
504
+ 不要简单依赖“对象 Key 很难猜”;随机 Key 只是减少枚举概率,不是访问控制。
505
+
506
+ ### 9.2 网络
507
+
508
+ 开发环境可按需要保留端口映射,生产环境必须:
509
+
510
+ - MinIO 9000 只允许 Nginx、NestJS 所在内网访问。
511
+ - MinIO 9001 控制台不代理、不暴露公网。
512
+ - 外部只开放 Nginx 的 HTTPS 入口。
513
+ - MinIO 根账号和应用账号分离,NestJS 使用最小权限服务账号。
514
+
515
+ ### 9.3 为什么不能只加 proxy_pass
516
+
517
+ 当前 uploads Bucket 是 private。普通 Nginx 不会自动给请求生成 AWS SigV4 签名,因此直接把匿名请求转发到私有 Bucket 会收到 AccessDenied。
518
+
519
+ 实现公开下载有三种方式:
520
+
521
+ | 方式 | 适用情况 | 本方案 |
522
+ | -------------------------- | ------------------ | -------------- |
523
+ | 独立 Bucket 匿名 GetObject | 文件真正公开 | 推荐 |
524
+ | 短期预签名 URL | 文件需要权限控制 | 私有文件时使用 |
525
+ | 后端/签名网关代理 | 需要复杂鉴权和审计 | 当前不采用 |
526
+
527
+ ## 10. Nginx 代理方案
528
+
529
+ 建议使用独立无 Cookie 域名:
530
+
531
+ ```text
532
+ files.example.com
533
+ ```
534
+
535
+ 如果暂时使用主站同源 /files,也必须对 HTML、SVG 等类型强制下载,并设置 nosniff。
536
+
537
+ 以下只表示目标语义,实施时需结合仓库实际 Nginx 目录落盘:
538
+
539
+ ```nginx
540
+ upstream minio_public {
541
+ server minio:9000;
542
+ keepalive 32;
543
+ }
544
+
545
+ location = /files {
546
+ return 404;
547
+ }
548
+
549
+ location = /files/ {
550
+ return 404;
551
+ }
552
+
553
+ location /files/ {
554
+ # Nginx 的 GET 规则同时允许 HEAD;其他方法全部拒绝。
555
+ limit_except GET {
556
+ deny all;
557
+ }
558
+
559
+ proxy_http_version 1.1;
560
+ proxy_pass http://minio_public/public-files/;
561
+ proxy_buffering off;
562
+
563
+ add_header X-Content-Type-Options nosniff always;
564
+ }
565
+ ```
566
+
567
+ 需要进一步验证:
568
+
569
+ - /files/a/b 能否精确映射为 public-files/a/b。
570
+ - Range 请求返回 206。
571
+ - If-None-Match、If-Modified-Since 能正常传递。
572
+ - Bucket 根路径不能列举。
573
+ - PUT、POST、PATCH、DELETE、multipart、ACL 请求全部失败。
574
+ - 路径穿越、双重编码、encoded slash 不会逃逸固定 Bucket。
575
+ - Nginx 不向客户端暴露不必要的 MinIO 内部错误和响应头。
576
+
577
+ 大文件下载建议增加:
578
+
579
+ - 每 IP 连接数限制。
580
+ - 请求速率和带宽监控。
581
+ - 合理超时。
582
+ - Range 请求滥用防护。
583
+
584
+ 防盗链只能减少普通外链流量,不能作为访问控制。
585
+
586
+ ## 11. 内容安全与缓存
587
+
588
+ ### 11.1 Content-Type
589
+
590
+ 当前 DTO 只验证 MIME 字符串格式,不能证明文件内容真实。
591
+
592
+ MVP 至少应:
593
+
594
+ - 建立允许上传的 MIME 白名单或用途级白名单。
595
+ - 不完全信任客户端 Content-Type。
596
+ - HTML、SVG、XML 等可执行或可嵌入类型强制 attachment。
597
+ - 设置 X-Content-Type-Options: nosniff。
598
+
599
+ 后续可增加文件签名检测、病毒扫描和内容审核。
600
+
601
+ ### 11.2 Content-Disposition
602
+
603
+ - 原始文件名必须清理控制字符和响应头注入字符。
604
+ - 同时提供安全的 ASCII fallback 和 RFC 5987 filename\*。
605
+ - fileName 存数据库;Content-Disposition 写入对象,保证 Nginx 直出时仍能获得正确文件名。
606
+
607
+ ### 11.3 缓存
608
+
609
+ 对象 Key 永不覆盖时才适合 immutable 缓存。
610
+
611
+ 本方案第一阶段建议使用较短缓存,待删除和 purge 流程验证后再延长。需要快速撤回的文件不要直接设置一年 immutable。
612
+
613
+ ## 12. 预计影响文件
614
+
615
+ 以下是实施阶段的预计范围,本次不会修改。
616
+
617
+ ### 12.1 数据库
618
+
619
+ - apps/server/src/database/schema.ts
620
+ - apps/server/drizzle/0002\_\*.sql
621
+ - 新增 files 表及必要枚举、索引和外键
622
+
623
+ 不得修改已经执行过的 0001 迁移,应新增迁移文件。
624
+
625
+ ### 12.2 上传模块
626
+
627
+ - apps/server/src/modules/upload/upload.controller.ts
628
+ - apps/server/src/modules/upload/upload.service.ts
629
+ - apps/server/src/modules/upload/upload.repository.ts
630
+ - apps/server/src/modules/upload/upload.module.ts
631
+ - apps/server/src/modules/upload/dto/\*
632
+ - apps/server/src/modules/upload/storage/storage.port.ts
633
+ - apps/server/src/modules/upload/storage/minio-storage.adapter.ts
634
+ - apps/server/src/modules/upload/utils/object-key.ts
635
+ - apps/server/src/modules/upload/upload.errors.ts
636
+
637
+ 可能新增:
638
+
639
+ - file.repository.ts
640
+ - response DTO 或序列化 Schema
641
+ - 文件名、后缀和 Content-Disposition 工具
642
+
643
+ ### 12.3 应用和配置
644
+
645
+ - apps/server/src/app.module.ts
646
+ - apps/server/src/config/storage.config.ts
647
+ - 环境变量示例文件
648
+ - docker-compose.yml
649
+ - 新增 Nginx 配置目录和服务
650
+
651
+ ### 12.4 测试
652
+
653
+ - 上传服务单元测试
654
+ - Repository/PostgreSQL 集成测试
655
+ - MinIO 集成测试
656
+ - API E2E 测试
657
+ - Nginx 公共访问安全测试
658
+ - test/error-catalog.spec.ts 中纳入 UPLOAD_ERRORS
659
+
660
+ ## 13. 分阶段实施顺序
661
+
662
+ ### 阶段 0:冻结业务决策
663
+
664
+ - 确认 projectId 是否永远必填。
665
+ - 确认哪些角色能上传、续传、完成、取消和删除。
666
+ - 确认允许公开的文件类型。
667
+ - 确认使用同源 /files 还是 files.example.com。
668
+ - 确认缓存和撤回时效。
669
+ - 确认是否存在需要迁移的历史上传数据。
670
+
671
+ ### 阶段 1:补齐 company/project 权限事实来源
672
+
673
+ - 建立 company、project、membership 模型或接入已有服务。
674
+ - 确保可以从 projectId 校验 companyId。
675
+ - 建立统一的公司/项目权限检查。
676
+
677
+ 在此阶段完成前,不应仅靠新增两个 DTO 字段就把 companyId、projectId 写入数据库。
678
+
679
+ ### 阶段 2:修复上传模块基线
680
+
681
+ - 合并 initiate/initate。
682
+ - 补齐 Controller 路由。
683
+ - 创建并注册 UploadModule。
684
+ - 修正参数 DTO。
685
+ - 统一 abort 响应。
686
+ - 修正相关文件名拼写及引用。
687
+ - 添加当前行为回归测试。
688
+
689
+ ### 阶段 3:新增 files 和加法式迁移
690
+
691
+ - 新增 files 表。
692
+ - upload_sessions 增加 fileId。
693
+ - 第一阶段保留旧列。
694
+ - 新上传同时写入 files 和 upload_sessions。
695
+ - 完成流程使用事务更新两张表。
696
+
697
+ 如果当前没有需要保留的生产数据,可以直接使用最终非空约束;如果已有数据,应先允许新字段为空并完成回填,再收紧约束。
698
+
699
+ ### 阶段 4:调整对象 Key 和响应契约
700
+
701
+ - 新对象改为不透明 Key。
702
+ - StoragePort 支持 Content-Disposition、Cache-Control 等需要的对象属性。
703
+ - 完成接口返回标准文件信息。
704
+ - fileUrl 根据配置动态生成。
705
+ - objectKey、bucket 不再出现在公开响应中。
706
+
707
+ ### 阶段 5:部署公共读取链路
708
+
709
+ - 新建 public-files Bucket。
710
+ - 设置匿名 GetObject 自定义策略。
711
+ - 新增 Nginx /files/\*\* 固定代理。
712
+ - 关闭生产环境 MinIO 公网端口。
713
+ - 验证 Range、安全头、方法限制和列举封锁。
714
+
715
+ ### 阶段 6:灰度和清理
716
+
717
+ - 先对测试环境和少量项目启用。
718
+ - 观察上传失败、恢复完成、孤儿对象和公网流量。
719
+ - 客户端完成适配后再删除旧响应字段。
720
+ - 回滚窗口结束后才考虑清理重复列和旧对象。
721
+
722
+ ## 14. 历史数据迁移
723
+
724
+ 如果 upload_sessions 已有 completed 数据:
725
+
726
+ 1. 新增 files 表和 upload_sessions.file_id,初始允许为空。
727
+ 2. 为每条已完成会话生成稳定 fileId。
728
+ 3. 从会话回填原文件名、类型、大小、bucket、objectKey、owner。
729
+ 4. companyId 只能根据可靠业务关系回填。
730
+ 5. projectId 不能从当前 objectKey 或文件名推断。
731
+ 6. 无法判断项目的记录进入人工映射清单,禁止猜测。
732
+ 7. 如需迁移到 public-files,采用复制、校验、切换、观察、再删除旧对象的顺序。
733
+ 8. 大文件使用 Multipart Copy 或重新上传,不能假设单次 CopyObject 可处理 90 GiB。
734
+ 9. 全部对账通过后再增加 NOT NULL、外键和最终唯一约束。
735
+
736
+ 迁移对账至少包含:
737
+
738
+ - 文件记录数。
739
+ - 对象数量。
740
+ - 总字节数。
741
+ - DB 有记录但对象缺失。
742
+ - 对象存在但 DB 无记录。
743
+ - company/project 缺失或不一致。
744
+
745
+ ## 15. 测试与验收
746
+
747
+ ### 15.1 字段与契约
748
+
749
+ - 完成响应包含 fileId、fileUrl、fileName、fileSuffix、fileSize、companyId、projectId。
750
+ - fileSuffix 为小写且不带点,无后缀为 null。
751
+ - fileSize 来自最终校验,不盲信客户端。
752
+ - 不返回 storageUploadId、bucket、objectKey、MinIO endpoint。
753
+ - 不返回重复的 uploadSize。
754
+ - 不增加含义不明的 group。
755
+ - complete 重试返回相同 fileId 和 fileUrl。
756
+
757
+ ### 15.2 权限
758
+
759
+ - 未登录不能初始化、上传分片、完成、取消或删除。
760
+ - 非公司成员不能上传。
761
+ - projectId 不属于 companyId 时拒绝。
762
+ - A 公司用户不能操作 B 公司的会话或文件。
763
+ - 修改 X-Company-Id 不能越权。
764
+ - 猜到 uploadSessionId 仍不能越权。
765
+
766
+ ### 15.3 上传一致性
767
+
768
+ - 初始化并发重试只创建一个 file 和 session。
769
+ - 分片大小错误、缺片、重复片按预期处理。
770
+ - Complete 并发只完成一次。
771
+ - MinIO Complete 成功、DB 临时失败后可以恢复。
772
+ - 取消和过期会话不会生成 available 文件。
773
+ - 已完成文件不能通过 abort 删除。
774
+
775
+ ### 15.4 公共访问
776
+
777
+ - 不携带 Authorization 和 Cookie 的 GET fileUrl 返回 200。
778
+ - HEAD 返回正确的 Content-Length、Content-Type。
779
+ - Range 返回 206。
780
+ - 未完成或不存在的 Key 返回 404。
781
+ - Bucket 根路径不能列举。
782
+ - PUT、POST、PATCH、DELETE 请求被拒绝。
783
+ - multipart、ACL、Bucket 管理操作被拒绝。
784
+ - 生产外网不能直连 9000、9001。
785
+ - 公共响应不泄露 companyId、projectId、ownerId、MinIO 凭据。
786
+ - HTML、SVG 等危险类型不会在主站同源直接执行。
787
+
788
+ ### 15.5 删除与缓存
789
+
790
+ - 删除接口只接受 fileId。
791
+ - 删除前执行公司、项目权限检查。
792
+ - 删除后公开 URL 按约定时效失效。
793
+ - CDN/Nginx 缓存存在时完成 purge 验证。
794
+ - 已被第三方下载的内容不承诺可收回。
795
+
796
+ ### 15.6 建议验证命令
797
+
798
+ 实施后至少运行:
799
+
800
+ ```bash
801
+ vp check
802
+ vp test
803
+ vp run -r build
804
+ ```
805
+
806
+ 并使用 curl 或自动化测试验证匿名 GET、HEAD、Range 和写方法封锁。
807
+
808
+ ## 16. 回滚方案
809
+
810
+ ### 16.1 应用
811
+
812
+ - 通过功能开关停止返回公共 fileUrl。
813
+ - 过渡期保留旧响应字段,避免旧客户端立即失效。
814
+ - 数据库采用加法式迁移,事故期间不删除新列、不回滚破坏性 DDL。
815
+
816
+ ### 16.2 Nginx 与 MinIO
817
+
818
+ - 下线 /files/\*\* 路由或统一返回 404。
819
+ - 撤销 public-files 的匿名 GetObject。
820
+ - 如已启用 CDN,执行全量 purge。
821
+ - 不立即删除 public-files 对象,先保留用于对账和恢复。
822
+
823
+ ### 16.3 对象迁移
824
+
825
+ - 复制失败时只清理本次明确创建的目标 Key。
826
+ - 已切换 DB 指针的文件应先回指旧对象,再删除新副本。
827
+ - 迁移任务记录 pending、copied、verified、switched 状态,保证可重跑。
828
+
829
+ 公开 URL 一旦被分享或文件被下载,技术回滚无法删除第三方已经保存的副本。
830
+
831
+ ## 17. 实施前待确认
832
+
833
+ 以下选项不影响本文档创建,但正式改代码前需要确认:
834
+
835
+ 1. projectId 是否始终必填;是否存在公司级文件。
836
+ 2. 用户是否只能管理自己上传的会话,项目管理员是否可以接管。
837
+ 3. 文件是否全部公开;是否需要 public/private 并存。
838
+ 4. 是否从第一版就使用独立域名 files.example.com。
839
+ 5. HTML、SVG、文本等类型是禁止上传,还是允许但强制下载。
840
+ 6. 缓存时间和文件撤回时效。
841
+ 7. 是否存在需要回填 companyId、projectId 的历史数据。
842
+
843
+ ## 18. 最终实施清单
844
+
845
+ - [ ] company/project/membership 权限来源明确
846
+ - [ ] projectId 必填规则确认
847
+ - [ ] 上传模块当前接通问题修复
848
+ - [ ] files 表和新迁移完成
849
+ - [ ] upload_sessions 关联 fileId
850
+ - [ ] 新对象 Key 不泄露用户和业务 ID
851
+ - [ ] 完成响应字段满足约定
852
+ - [ ] uploadSize、group 不进入完成响应
853
+ - [ ] 完成幂等和存储恢复通过测试
854
+ - [ ] public-files 仅匿名 GetObject
855
+ - [ ] Nginx 仅开放 GET、HEAD
856
+ - [ ] MinIO 9000、9001 不暴露公网
857
+ - [ ] Range、Content-Disposition、nosniff 验证通过
858
+ - [ ] 删除、缓存和回滚流程演练通过