Browse Source

删掉写错了的 统一文件模块包

yourname 2 weeks ago
parent
commit
ea0f60c10a
26 changed files with 0 additions and 3175 deletions
  1. 0 340
      docs/stories/010.009.story.md
  2. 0 207
      docs/stories/010.010.story.md
  3. 0 209
      docs/stories/010.011.story.md
  4. 0 39
      packages/unified-file-module/package.json
  5. 0 1
      packages/unified-file-module/src/entities/index.ts
  6. 0 118
      packages/unified-file-module/src/entities/unified-file.entity.ts
  7. 0 4
      packages/unified-file-module/src/index.ts
  8. 0 77
      packages/unified-file-module/src/routes/[id]/delete.ts
  9. 0 83
      packages/unified-file-module/src/routes/[id]/download.ts
  10. 0 75
      packages/unified-file-module/src/routes/[id]/get-url.ts
  11. 0 443
      packages/unified-file-module/src/routes/admin/unified-files.admin.routes.ts
  12. 0 37
      packages/unified-file-module/src/routes/index.ts
  13. 0 142
      packages/unified-file-module/src/routes/multipart-complete/post.ts
  14. 0 138
      packages/unified-file-module/src/routes/multipart-policy/post.ts
  15. 0 89
      packages/unified-file-module/src/routes/upload-policy/post.ts
  16. 0 1
      packages/unified-file-module/src/schemas/index.ts
  17. 0 101
      packages/unified-file-module/src/schemas/unified-file.schema.ts
  18. 0 2
      packages/unified-file-module/src/services/index.ts
  19. 0 200
      packages/unified-file-module/src/services/minio.service.ts
  20. 0 463
      packages/unified-file-module/src/services/unified-file.service.ts
  21. 0 154
      packages/unified-file-module/tests/integration/unified-files.integration.test.ts
  22. 0 75
      packages/unified-file-module/tests/unit/unified-file.service.unit.test.ts
  23. 0 34
      packages/unified-file-module/tests/utils/integration-test-db.ts
  24. 0 106
      packages/unified-file-module/tests/utils/integration-test-utils.ts
  25. 0 16
      packages/unified-file-module/tsconfig.json
  26. 0 21
      packages/unified-file-module/vitest.config.ts

+ 0 - 340
docs/stories/010.009.story.md

@@ -1,340 +0,0 @@
-# Story 010.009: 创建统一文件后端模块
-
-## Status
-In Progress
-
-## Story
-
-**As a** 超级管理员,
-**I want** 一个统一的后端文件模块(无租户隔离),
-**so that** 可以在租户管理后台统一管理所有文件,统一广告模块使用的文件与其他统一模块保持一致的数据隔离模式。
-
-## Background
-
-**当前问题**:
-- 统一广告模块 (`unified-advertisements-module`) 使用 `@d8d/core-module-mt/file-module-mt` 的 `FileMt` 实体(多租户,有tenant_id)
-- **不一致性**: 统一广告本身是无租户隔离的,但关联的文件却是多租户隔离的
-
-**解决方案**:
-- 参照统一广告模块的设计模式,从单租户的 `file-module` 复制创建统一版本
-- 创建 `unified-file-module`(无tenant_id字段)
-- 统一广告模块更新为使用 `UnifiedFile` 实体
-
-## Acceptance Criteria
-
-1. 创建 `packages/unified-file-module` 包,从 `file-module` 复制并改造为无租户隔离版本
-2. Entity定义不包含 `tenant_id` 字段,与原 `file-module` 的Entity结构相同但移除租户隔离
-3. 实现管理员路由(使用 `tenantAuthMiddleware`),只有超级管理员(ID=1)可访问
-4. 实现文件上传功能(MinIO集成)
-5. 包含完整的单元测试和集成测试
-
-## Tasks / Subtasks
-
-- [x] **任务1: 创建包结构和配置文件** (AC: 1)
-  - [x] 创建 `packages/unified-file-module` 目录
-  - [x] 创建 `package.json`,配置包名为 `@d8d/unified-file-module`
-  - [x] 创建 `tsconfig.json`
-  - [x] 创建 `vitest.config.ts`(设置 `fileParallelism: false`)
-  - [x] 创建 `src/` 子目录:`entities/`, `services/`, `routes/`, `schemas/`
-  - [x] 创建 `tests/` 子目录:`integration/`, `unit/`, `utils/`
-
-- [x] **任务2: 定义Entity(无tenant_id字段)** (AC: 2)
-  - [x] 创建 `src/entities/unified-file.entity.ts`,参考 `file-module` 但移除 `tenant_id` 字段
-  - [x] 确保字段包含:`id`, `fileName`, `filePath`, `fileSize`, `mimeType`, `status`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`
-  - [x] 添加 `@Index` 索引(status, createdAt等)
-  - [x] 创建 `src/entities/index.ts` 导出Entity
-
-- [x] **任务3: 实现Service层** (AC: 1)
-  - [x] 创建 `src/services/unified-file.service.ts`,继承 `GenericCrudService`
-  - [x] 覆盖 `create`、`update`、`delete` 方法(使用 `override` 关键字)
-  - [x] 实现软删除逻辑(设置 `status=0`)
-  - [x] 实现文件上传逻辑(调用MinIO)
-  - [x] 创建 `src/services/index.ts` 导出Service
-
-- [x] **任务4: 定义Schema** (AC: 1)
-  - [x] 创建 `src/schemas/unified-file.schema.ts`,使用 Zod + OpenAPI装饰器
-  - [x] 使用 `z.coerce.date<Date>()` 和 `z.coerce.number<number>()` 泛型语法
-  - [x] 定义 `CreateUnifiedFileDto`、`UpdateUnifiedFileDto`、`UnifiedFileListResponseSchema`
-  - [x] 不导出推断类型(`z.infer`),类型由RPC自动推断
-  - [x] 创建 `src/schemas/index.ts` 导出Schema
-
-- [x] **任务5: 实现管理员路由** (AC: 3)
-  - [x] 创建 `src/routes/admin/unified-files.admin.routes.ts`
-  - [x] 使用 `OpenAPIHono` 和 `AuthContext` 泛型
-  - [x] 使用 `createRoute` 定义路由,包含请求/响应Schema
-  - [x] 应用 `tenantAuthMiddleware` 中间件(只有超级管理员可访问)
-  - [x] 自定义路由使用 `parseWithAwait` 验证响应数据
-  - [x] 使用 `createZodErrorResponse` 处理Zod错误
-
-- [x] **任务6: 实现文件上传处理** (AC: 4)
-  - [x] 实现MinIO文件上传逻辑
-  - [x] 实现文件验证(大小、类型)
-  - [x] 实现文件删除(MinIO + 数据库)
-  - [x] 实现文件URL生成
-
-- [x] **任务7: 创建包导出入口** (AC: 1)
-  - [x] 创建 `src/index.ts`,导出Entities、Services、Routes、Schemas
-  - [x] 配置 `package.json` 的 `exports` 字段
-
-- [x] **任务8: 编写单元测试** (AC: 5)
-  - [x] 创建Service层单元测试
-  - [x] 测试软删除逻辑
-
-- [x] **任务9: 编写集成测试** (AC: 5)
-  - [x] 创建 `tests/integration/unified-files.integration.test.ts`
-  - [x] 测试管理员CRUD操作(验证 `tenantAuthMiddleware` 权限)
-  - [x] 测试文件上传功能
-  - [x] 测试文件删除功能
-
-- [x] **任务10: 代码质量检查**
-  - [x] 运行 `pnpm test` 确保所有测试通过 (8/8 通过)
-
----
-
-## 补充任务(发现缺失功能后添加)
-
-- [ ] **任务11: 创建文件上传策略路由** (AC: 4)
-  - [ ] 创建 `src/routes/upload-policy/post.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 MinIO 上传策略生成(含签名)
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-  - [ ] 定义 Schema 包含 uploadPolicy 响应结构
-
-- [ ] **任务12: 创建分片上传策略路由** (AC: 4)
-  - [ ] 创建 `src/routes/multipart-policy/post.ts`,参考 `file-module` 对应文件
-  - [ ] 实现分片上传策略生成
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-
-- [ ] **任务13: 创建分片上传完成路由** (AC: 4)
-  - [ ] 创建 `src/routes/multipart-complete/post.ts`,参考 `file-module` 对应文件
-  - [ ] 实现分片上传完成逻辑
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-
-- [ ] **任务14: 创建获取文件URL路由** (AC: 4)
-  - [ ] 创建 `src/routes/[id]/get-url.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 `GET /:id/url` 端点
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-
-- [ ] **任务15: 创建文件删除路由** (AC: 4)
-  - [ ] 创建 `src/routes/[id]/delete.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 `DELETE /:id` 端点
-  - [ ] 同时删除 MinIO 文件和数据库记录
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-
-- [ ] **任务16: 创建文件下载路由** (AC: 4)
-  - [ ] 创建 `src/routes/[id]/download.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 `GET /:id/download` 端点(返回带 Content-Disposition 的 URL)
-  - [ ] 使用 `tenantAuthMiddleware` 中间件
-
-- [ ] **任务17: 更新路由入口文件** (AC: 4)
-  - [ ] 更新 `src/routes/index.ts`,聚合所有路由
-  - [ ] 添加 `createCrudRoutes` 生成基础 CRUD 路由
-  - [ ] 导出完整的 `unifiedFileRoutes` 路由集合
-
-- [ ] **任务18: 创建测试工具文件**
-  - [ ] 创建 `tests/utils/integration-test-utils.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 `IntegrationTestAssertions` 类(expectFileToExist 等方法)
-  - [ ] 适配 `UnifiedFile` 实体
-
-- [ ] **任务19: 创建测试数据工厂**
-  - [ ] 创建 `tests/utils/integration-test-db.ts`,参考 `file-module` 对应文件
-  - [ ] 实现 `TestDataFactory` 类
-  - [ ] 适配 `UnifiedFile` 实体字段
-
-- [ ] **任务20: 补充集成测试**
-  - [ ] 为上传策略路由添加集成测试
-  - [ ] 为分片上传路由添加集成测试
-  - [ ] 为获取URL路由添加集成测试
-  - [ ] 为下载路由添加集成测试
-  - [ ] 为删除路由添加集成测试
-
-- [ ] **任务21: 完整回归测试**
-  - [ ] 运行 `pnpm test` 确保所有测试通过
-  - [ ] 运行 `pnpm typecheck` 确保无类型错误
-  - [ ] 更新 File List 列出所有新增文件
-
-## Dev Notes
-
-### 项目结构信息
-
-**新包位置**:
-```
-packages/unified-file-module/
-├── package.json
-├── tsconfig.json
-├── vitest.config.ts
-├── src/
-│   ├── entities/
-│   │   ├── unified-file.entity.ts
-│   │   └── index.ts
-│   ├── services/
-│   │   ├── unified-file.service.ts
-│   │   └── index.ts
-│   ├── routes/
-│   │   ├── admin/
-│   │   │   └── unified-files.admin.routes.ts
-│   │   └── index.ts
-│   ├── schemas/
-│   │   ├── unified-file.schema.ts
-│   │   └── index.ts
-│   └── index.ts
-└── tests/
-    ├── integration/
-    │   └── unified-files.integration.test.ts
-    ├── unit/
-    │   └── unified-file.service.test.ts
-    └── utils/
-        └── test-data-factory.ts
-```
-
-**参考模块**:
-- 单租户文件模块: `packages/file-module`
-- 统一广告模块: `packages/unified-advertisements-module`
-- 认证模块: `@d8d/core-module-mt/auth-module-mt`
-- 租户模块: `@d8d/tenant-module-mt`
-
-### Entity设计规范
-
-**统一文件Entity** (`unified-file.entity.ts`):
-- 继承自原 `file-module` 的Entity结构
-- **关键区别**: 无 `tenant_id` 字段
-- 字段包括:`id`, `fileName`, `filePath`, `fileSize`, `mimeType`, `status`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`
-
-### 路由设计规范
-
-**管理员接口** (完整列表):
-```
-# 文件上传策略
-POST   /api/v1/admin/unified-files/upload-policy        # 生成MinIO上传策略
-POST   /api/v1/admin/unified-files/multipart-policy    # 生成分片上传策略
-POST   /api/v1/admin/unified-files/multipart-complete  # 完成分片上传
-
-# 文件操作(通过createCrudRoutes自动生成)
-GET    /api/v1/admin/unified-files              # 文件列表
-POST   /api/v1/admin/unified-files              # 创建文件记录
-GET    /api/v1/admin/unified-files/:id          # 获取文件详情
-PUT    /api/v1/admin/unified-files/:id          # 更新文件记录
-
-# 文件专用操作
-GET    /api/v1/admin/unified-files/:id/url      # 获取文件访问URL
-GET    /api/v1/admin/unified-files/:id/download # 获取文件下载URL
-DELETE /api/v1/admin/unified-files/:id          # 删除文件(MinIO+DB)
-```
-
-### 中间件使用规范
-
-**管理员路由**:
-- 使用 `tenantAuthMiddleware`(来自 `@d8d/tenant-module-mt`,独立包)
-- 只有超级管理员(ID=1)可访问
-
-### MinIO集成
-
-**文件上传流程**:
-1. 接收文件上传请求
-2. 验证文件大小和类型
-3. 上传到MinIO
-4. 保存文件记录到数据库
-5. 返回文件信息
-
-### 测试标准
-
-| 测试类型 | 最低要求 | 目标要求 |
-|----------|----------|----------|
-| 单元测试 | 70% | 80% |
-| 集成测试 | 50% | 60% |
-
-### 关键测试要求
-1. **权限测试**: 验证管理员路由只有超级管理员可访问
-2. **文件上传测试**: 验证MinIO文件上传和数据库记录
-3. **软删除测试**: 验证删除操作设置 `status=0` 而非物理删除
-4. **文件验证测试**: 验证文件大小、类型限制
-
-## Testing
-
-### 测试文件位置
-- 单元测试: `tests/unit/`
-- 集成测试: `tests/integration/`
-- 测试工具: `tests/utils/`
-
-### 测试框架
-- **Vitest**: 主要测试运行器
-- **hono/testing**: API路由测试
-- **TypeORM**: 数据库测试
-
-### 测试执行命令
-```bash
-# 进入模块目录
-cd packages/unified-file-module
-
-# 运行所有测试
-pnpm test
-
-# 运行集成测试
-pnpm test:integration
-
-# 生成覆盖率报告
-pnpm test:coverage
-
-# 类型检查
-pnpm typecheck
-```
-
-## Change Log
-
-| Date | Version | Description | Author |
-|------|---------|-------------|--------|
-| 2026-01-03 | 1.0 | 初始故事创建 | James (Claude Code) |
-| 2026-01-04 | 1.1 | 修复阶段 - 修复vitest配置、Entity、Service、Schema、测试 | James (Claude Code) |
-| 2026-01-04 | 1.2 | **发现缺失功能** - 添加补充任务11-21,补充完整的路由功能(上传策略、分片上传、URL获取、下载、删除)和测试工具 | James (Claude Code) |
-
-## Dev Agent Record
-
-### Agent Model Used
-claude-opus-4-5-20251101 (via Happy)
-
-### Debug Log References
-无特殊调试需求,所有测试一次性通过。
-
-### Completion Notes List
-1. **修复了初始创建的代码问题**:
-   - vitest.config.ts 删除了不必要的 resolve.alias 配置(pnpm workspace 自动解析)
-   - unified-file.entity.ts 补充完整的 Entity 类定义
-   - unified-file.service.ts 补充完整的 Service 实现
-   - unified-file.schema.ts 修正字段名与 Entity 一致(fileName 而非 name)
-
-2. **集成测试设计**:
-   - 不需要 UserEntityMt 依赖(统一文件模块只用 tenantAuthMiddleware 验证 JWT)
-   - 只测试超级管理员权限(ID=1)访问
-
-3. **测试结果**:
-   - 单元测试: 2/2 通过
-   - 集成测试: 6/6 通过
-   - 总计: 8/8 通过
-
-4. **🚨 发现严重问题 (2026-01-04)**:
-   - 原始 `file-module` 包含完整的文件上传功能(upload-policy, multipart-policy, multipart-complete)
-   - 原始 `file-module` 包含文件URL获取和下载功能(get-url, download)
-   - 原始 `file-module` 包含独立的删除路由(delete.ts)
-   - **unified-file-module 缺失上述所有路由功能!**
-   - 缺失测试工具文件:`integration-test-utils.ts`, `integration-test-db.ts`
-   - 已添加补充任务11-21来完善功能
-
-### File List
-**新增文件**:
-- `package.json` - 包配置,依赖 @d8d/shared-crud, @d8d/shared-utils, @d8d/tenant-module-mt
-- `tsconfig.json` - TypeScript 配置
-- `vitest.config.ts` - Vitest 测试配置(无别名配置)
-- `src/entities/unified-file.entity.ts` - 统一文件实体(无 tenant_id)
-- `src/entities/index.ts` - Entity 导出
-- `src/services/unified-file.service.ts` - Service 层(软删除实现)
-- `src/services/minio.service.ts` - MinIO 文件服务
-- `src/services/index.ts` - Service 导出
-- `src/schemas/unified-file.schema.ts` - Zod Schema 定义
-- `src/schemas/index.ts` - Schema 导出
-- `src/routes/admin/unified-files.admin.routes.ts` - 管理员路由(tenantAuthMiddleware)
-- `src/routes/index.ts` - Routes 导出
-- `src/index.ts` - 包主入口
-- `tests/integration/unified-files.integration.test.ts` - 集成测试
-- `tests/unit/unified-file.service.unit.test.ts` - 单元测试
-
-## QA Results
-_待QA代理填写_

+ 0 - 207
docs/stories/010.010.story.md

@@ -1,207 +0,0 @@
-# Story 010.010: 创建统一文件管理UI包
-
-## Status
-Approved
-
-## Story
-
-**As a** 超级管理员,
-**I want** 一个统一的文件管理UI包,
-**so that** 可以在租户管理后台统一管理所有文件,统一广告管理UI使用统一的文件选择器组件。
-
-## Background
-
-**当前问题**:
-- 统一广告管理UI (`unified-advertisement-management-ui`) 使用 `@d8d/file-management-ui-mt` 的 `FileSelector` 组件(多租户)
-- **不一致性**: 统一广告管理UI本身是无租户隔离的,但使用的文件选择器却是多租户版本
-
-**解决方案**:
-- 参照统一广告管理UI的设计模式,从单租户的 `file-management-ui` 复制创建统一版本
-- 创建 `unified-file-management-ui`(API指向统一文件模块)
-
-## Acceptance Criteria
-
-1. 创建 `packages/unified-file-management-ui` 包,从 `file-management-ui` 复制并改造为指向统一文件模块
-2. 实现文件管理组件(列表、上传、删除)
-3. 实现文件选择器组件(供其他UI包使用)
-4. API客户端指向统一文件模块端点(`/api/v1/admin/unified-files`)
-5. 包含完整的组件测试和集成测试
-
-## Tasks / Subtasks
-
-- [ ] **任务1: 创建UI包结构和配置文件** (AC: 1)
-  - [ ] 创建 `packages/unified-file-management-ui` 目录
-  - [ ] 创建 `package.json`,配置包名为 `@d8d/unified-file-management-ui`
-  - [ ] 创建 `tsconfig.json`
-  - [ ] 创建 `vitest.config.ts`(设置 `fileParallelism: false`,配置别名)
-  - [ ] 创建 `src/` 子目录:`components/`, `api/`, `hooks/`, `types/`, `utils/`
-  - [ ] 创建 `tests/` 子目录:`integration/`, `unit/`
-
-- [ ] **任务2: 创建API客户端** (AC: 4)
-  - [ ] 创建 `src/api/client.ts`,使用 `hc` RPC客户端
-  - [ ] 配置API端点指向 `/api/v1/admin/unified-files`
-  - [ ] 创建类型定义(从RPC推断)
-  - [ ] 创建API方法:`uploadFile`, `getFiles`, `getFile`, `deleteFile`
-
-- [ ] **任务3: 创建React Hooks** (AC: 1)
-  - [ ] 创建 `src/hooks/useUnifiedFiles.ts`(文件列表)
-  - [ ] 创建 `src/hooks/useUnifiedFileUpload.ts`(文件上传)
-  - [ ] 创建 `src/hooks/useUnifiedFileDelete.ts`(文件删除)
-
-- [ ] **任务4: 实现文件管理组件** (AC: 2)
-  - [ ] 创建 `src/components/UnifiedFileList.tsx`(文件列表)
-  - [ ] 创建 `src/components/UnifiedFileUpload.tsx`(文件上传)
-  - [ ] 创建 `src/components/UnifiedFileManagement.tsx`(管理页面主组件)
-
-- [ ] **任务5: 实现文件选择器组件** (AC: 3)
-  - [ ] 创建 `src/components/UnifiedFileSelector.tsx`(文件选择器)
-  - [ ] 支持单选和多选模式
-  - [ ] 支持文件类型过滤
-  - [ ] 支持搜索功能
-  - [ ] 添加 `data-testid` 属性用于测试
-
-- [ ] **任务6: 创建包导出入口** (AC: 1)
-  - [ ] 创建 `src/index.ts`,导出组件、Hooks、API客户端
-  - [ ] 配置 `package.json` 的 `exports` 字段
-
-- [ ] **任务7: 编写组件测试** (AC: 5)
-  - [ ] 创建 `tests/unit/UnifiedFileSelector.test.tsx`
-  - [ ] 创建 `tests/unit/UnifiedFileList.test.tsx`
-  - [ ] 创建 `tests/unit/UnifiedFileUpload.test.tsx`
-
-- [ ] **任务8: 编写集成测试** (AC: 5)
-  - [ ] 创建 `tests/integration/file-selector.integration.test.tsx`
-  - [ ] 创建 `tests/integration/file-upload.integration.test.tsx`
-  - [ ] 创建 `tests/integration/file-management.integration.test.tsx`
-
-- [ ] **任务9: 代码质量检查**
-  - [ ] 运行 `pnpm typecheck` 确保无TypeScript错误
-  - [ ] 运行 `pnpm lint` 确保代码符合规范
-  - [ ] 运行 `pnpm test` 确保所有测试通过
-  - [ ] 运行 `pnpm test:coverage` 确保覆盖率达标
-
-## Dev Notes
-
-### 项目结构信息
-
-**新包位置**:
-```
-packages/unified-file-management-ui/
-├── package.json
-├── tsconfig.json
-├── vitest.config.ts
-├── src/
-│   ├── components/
-│   │   ├── UnifiedFileList.tsx
-│   │   ├── UnifiedFileUpload.tsx
-│   │   ├── UnifiedFileSelector.tsx
-│   │   └── UnifiedFileManagement.tsx
-│   ├── api/
-│   │   └── client.ts
-│   ├── hooks/
-│   │   ├── useUnifiedFiles.ts
-│   │   ├── useUnifiedFileUpload.ts
-│   │   └── useUnifiedFileDelete.ts
-│   ├── types/
-│   │   └── index.ts
-│   ├── utils/
-│   │   └── file-utils.ts
-│   └── index.ts
-└── tests/
-    ├── integration/
-    │   ├── file-selector.integration.test.tsx
-    │   ├── file-upload.integration.test.tsx
-    │   └── file-management.integration.test.tsx
-    └── unit/
-        ├── UnifiedFileSelector.test.tsx
-        ├── UnifiedFileList.test.tsx
-        └── UnifiedFileUpload.test.tsx
-```
-
-**参考模块**:
-- 单租户文件管理UI: `packages/file-management-ui`
-- 统一广告管理UI: `packages/unified-advertisement-management-ui`
-
-### API客户端设计
-
-**RPC客户端使用**:
-```typescript
-import { hc } from '@d8d/shared-ui-components/utils/hc';
-import type { UnifiedFileRoutes } from '@d8d/unified-file-module';
-
-export const unifiedFileClient = hc<UnifiedFileRoutes>('/api/v1/admin/unified-files');
-```
-
-### 组件设计规范
-
-**文件选择器** (`UnifiedFileSelector.tsx`):
-- 使用RPC推断类型(不直接导入schema类型)
-- 添加 `data-testid` 属性用于测试
-- 支持单选和多选模式
-- 支持文件类型过滤
-- 支持搜索功能
-
-### 测试标准
-
-| 测试类型 | 最低要求 | 目标要求 |
-|----------|----------|----------|
-| 单元测试 | 70% | 80% |
-| 集成测试 | 50% | 60% |
-
-### 关键测试要求
-1. **组件测试**: 验证组件渲染正确
-2. **交互测试**: 验证用户交互(上传、删除、选择)
-3. **API集成测试**: 验证API调用正确
-4. **文件选择器测试**: 验证选择器功能(单选、多选、搜索)
-
-## Testing
-
-### 测试文件位置
-- 单元测试: `tests/unit/`
-- 集成测试: `tests/integration/`
-
-### 测试框架
-- **Vitest**: 主要测试运行器
-- **Testing Library**: React组件测试
-- **Happy DOM**: 轻量级DOM环境
-
-### 测试执行命令
-```bash
-# 进入UI包目录
-cd packages/unified-file-management-ui
-
-# 运行所有测试
-pnpm test
-
-# 运行集成测试
-pnpm test:integration
-
-# 生成覆盖率报告
-pnpm test:coverage
-
-# 类型检查
-pnpm typecheck
-```
-
-## Change Log
-
-| Date | Version | Description | Author |
-|------|---------|-------------|--------|
-| 2026-01-03 | 1.0 | 初始故事创建 | James (Claude Code) |
-
-## Dev Agent Record
-
-### Agent Model Used
-_待开发时填写_
-
-### Debug Log References
-_待开发时填写_
-
-### Completion Notes List
-_待开发时填写_
-
-### File List
-_待开发时填写_
-
-## QA Results
-_待QA代理填写_

+ 0 - 209
docs/stories/010.011.story.md

@@ -1,209 +0,0 @@
-# Story 010.011: 集成统一文件模块到统一广告和租户后台
-
-## Status
-Approved
-
-## Story
-
-**As a** 超级管理员,
-**I want** 统一文件模块集成到统一广告模块和租户后台,
-**so that** 可以在租户管理后台统一管理所有文件,统一广告模块使用统一文件实体。
-
-## Background
-
-**前置条件**:
-- 故事010.009已完成:统一文件模块 (`unified-file-module`) 已创建
-- 故事010.010已完成:统一文件管理UI (`unified-file-management-ui`) 已创建
-
-**需要完成的集成**:
-1. 统一广告模块更新为使用 `UnifiedFile` 实体(而非 `FileMt`)
-2. 统一广告管理UI更新为使用统一文件选择器(而非多租户版本)
-3. Server包注册统一文件模块路由
-4. 租户后台集成统一文件管理功能
-
-## Acceptance Criteria
-
-1. 统一广告模块更新Entity关联,使用 `UnifiedFile` 替代 `FileMt`
-2. 统一广告管理UI更新依赖,使用 `@d8d/unified-file-management-ui`
-3. Server包注册统一文件模块路由和实体
-4. 租户后台添加文件管理菜单项和路由
-5. 统一广告模块所有测试通过
-6. 统一广告管理UI所有测试通过
-7. E2E测试验证文件上传和选择器功能
-
-## Tasks / Subtasks
-
-- [ ] **任务1: 更新统一广告模块Entity** (AC: 1)
-  - [ ] 修改 `unified-advertisement.entity.ts`,将 `FileMt` 替换为 `UnifiedFile`
-  - [ ] 更新导入语句:`import { UnifiedFile } from '@d8d/unified-file-module'`
-  - [ ] 更新 `@ManyToOne` 关联定义
-  - [ ] 更新 `@JoinColumn` 定义
-
-- [ ] **任务2: 更新统一广告模块依赖** (AC: 1)
-  - [ ] 修改 `package.json`,添加 `@d8d/unified-file-module` 依赖
-  - [ ] 移除或保留 `@d8d/core-module-mt` 依赖(如果其他模块还需要)
-  - [ ] 运行 `pnpm install` 更新依赖
-
-- [ ] **任务3: 更新统一广告模块测试** (AC: 5)
-  - [ ] 更新测试文件中的 `FileMt` 引用为 `UnifiedFile`
-  - [ ] 更新测试数据工厂
-  - [ ] 运行测试验证修改正确
-
-- [ ] **任务4: 更新统一广告管理UI依赖** (AC: 2)
-  - [ ] 修改 `package.json`,添加 `@d8d/unified-file-management-ui` 依赖
-  - [ ] 移除 `@d8d/file-management-ui-mt` 依赖
-  - [ ] 更新 `vitest.config.ts` 别名配置
-  - [ ] 运行 `pnpm install` 更新依赖
-
-- [ ] **任务5: 更新统一广告管理UI组件** (AC: 2)
-  - [ ] 修改 `UnifiedAdvertisementManagement.tsx`,将 `FileSelector` 替换为 `UnifiedFileSelector`
-  - [ ] 更新导入语句
-  - [ ] 更新组件props(如果有差异)
-
-- [ ] **任务6: 更新统一广告管理UI测试** (AC: 6)
-  - [ ] 更新所有测试文件中的mock:`@d8d/file-management-ui-mt` → `@d8d/unified-file-management-ui`
-  - [ ] 更新mock组件名称
-  - [ ] 运行测试验证修改正确
-
-- [ ] **任务7: Server包集成统一文件模块** (AC: 3)
-  - [ ] 在 `packages/server/src/index.ts` 中导入统一文件模块
-  - [ ] 导入 `UnifiedFile` 实体
-  - [ ] 导入统一文件管理员路由
-  - [ ] 注册 `UnifiedFile` 实体到数据源
-  - [ ] 注册管理员路由:`app.route('/api/v1/admin/unified-files', unifiedFileAdminRoutes)`
-
-- [ ] **任务8: 租户后台集成统一文件管理** (AC: 4)
-  - [ ] 在 `web/src/client/tenant/` 添加文件管理路由
-  - [ ] 在 `web/src/client/tenant/` 添加文件管理页面组件
-  - [ ] 添加文件管理菜单项(Files图标)
-  - [ ] 初始化API客户端(指向管理员API)
-
-- [ ] **任务9: 编写E2E测试** (AC: 7)
-  - [ ] 创建 `web/tests/e2e/tenant-file-management.spec.ts`
-  - [ ] 测试文件上传流程
-  - [ ] 测试文件列表显示
-  - [ ] 测试文件删除功能
-  - [ ] 测试文件选择器在广告管理中的使用
-
-- [ ] **任务10: 代码质量检查**
-  - [ ] 运行 `pnpm typecheck` 确保无TypeScript错误
-  - [ ] 运行 `pnpm lint` 确保代码符合规范
-  - [ ] 运行所有测试确保通过
-  - [ ] 更新史诗010文档
-
-## Dev Notes
-
-### 修改的文件清单
-
-**统一广告模块** (`packages/unified-advertisements-module/`):
-- `src/entities/unified-advertisement.entity.ts` - 更新Entity关联
-- `src/entities/index.ts` - 更新导入
-- `package.json` - 添加依赖
-- `tests/**/*.test.ts` - 更新测试
-
-**统一广告管理UI** (`packages/unified-advertisement-management-ui/`):
-- `src/components/UnifiedAdvertisementManagement.tsx` - 更新组件
-- `package.json` - 更新依赖
-- `vitest.config.ts` - 更新别名配置
-- `tests/**/*.test.tsx` - 更新mock
-
-**Server包** (`packages/server/`):
-- `src/index.ts` - 注册路由和实体
-- `package.json` - 添加依赖
-
-**租户后台** (`web/src/client/tenant/`):
-- 添加路由配置
-- 添加页面组件
-- 更新菜单
-
-### Entity关联更新
-
-**更新前**:
-```typescript
-import { FileMt } from '@d8d/core-module-mt/file-module-mt';
-
-@Entity('unified_advertisement')
-export class UnifiedAdvertisement {
-  // ...
-
-  @ManyToOne(() => FileMt, { nullable: true })
-  @JoinColumn({ name: 'image_file_id' })
-  imageFile!: FileMt | null;
-}
-```
-
-**更新后**:
-```typescript
-import { UnifiedFile } from '@d8d/unified-file-module';
-
-@Entity('unified_advertisement')
-export class UnifiedAdvertisement {
-  // ...
-
-  @ManyToOne(() => UnifiedFile, { nullable: true })
-  @JoinColumn({ name: 'image_file_id' })
-  imageFile!: UnifiedFile | null;
-}
-```
-
-### 测试标准
-
-| 测试类型 | 要求 |
-|----------|------|
-| 统一广告模块测试 | 全部通过 |
-| 统一广告管理UI测试 | 全部通过 |
-| Server包集成测试 | 全部通过 |
-| E2E测试 | 全部通过 |
-
-### 关键测试要求
-1. **回归测试**: 确保统一广告模块原有功能不受影响
-2. **关联测试**: 验证 `UnifiedFile` 关联正确
-3. **集成测试**: 验证Server包路由注册正确
-4. **UI测试**: 验证文件选择器在广告管理中正常工作
-5. **E2E测试**: 验证完整的文件管理流程
-
-## Testing
-
-### 测试文件位置
-- E2E测试: `web/tests/e2e/`
-
-### 测试框架
-- **Playwright**: E2E测试
-
-### 测试执行命令
-```bash
-# 统一广告模块测试
-cd packages/unified-advertisements-module
-pnpm test
-
-# 统一广告管理UI测试
-cd packages/unified-advertisement-management-ui
-pnpm test
-
-# E2E测试
-cd web
-pnpm test:e2e:chromium
-```
-
-## Change Log
-
-| Date | Version | Description | Author |
-|------|---------|-------------|--------|
-| 2026-01-03 | 1.0 | 初始故事创建 | James (Claude Code) |
-
-## Dev Agent Record
-
-### Agent Model Used
-_待开发时填写_
-
-### Debug Log References
-_待开发时填写_
-
-### Completion Notes List
-_待开发时填写_
-
-### File List
-_待开发时填写_
-
-## QA Results
-_待QA代理填写_

+ 0 - 39
packages/unified-file-module/package.json

@@ -1,39 +0,0 @@
-{
-  "name": "@d8d/unified-file-module",
-  "version": "1.0.0",
-  "description": "统一文件管理模块(无租户隔离)",
-  "type": "module",
-  "main": "src/index.ts",
-  "types": "src/index.ts",
-  "exports": {
-    ".": { "types": "./src/index.ts", "import": "./src/index.ts" },
-    "./entities": { "types": "./src/entities/index.ts", "import": "./src/entities/index.ts" },
-    "./services": { "types": "./src/services/index.ts", "import": "./src/services/index.ts" },
-    "./schemas": { "types": "./src/schemas/index.ts", "import": "./src/schemas/index.ts" },
-    "./routes": { "types": "./src/routes/index.ts", "import": "./src/routes/index.ts" }
-  },
-  "scripts": {
-    "build": "tsc",
-    "test": "vitest run",
-    "test:unit": "vitest run tests/unit",
-    "test:integration": "vitest run tests/integration",
-    "typecheck": "tsc --noEmit"
-  },
-  "dependencies": {
-    "@d8d/shared-crud": "workspace:*",
-    "@d8d/shared-types": "workspace:*",
-    "@d8d/shared-utils": "workspace:*",
-    "@d8d/tenant-module-mt": "workspace:*",
-    "@hono/zod-openapi": "^1.0.2",
-    "hono": "^4.8.5",
-    "minio": "^8.0.5",
-    "typeorm": "^0.3.20",
-    "uuid": "^11.1.0",
-    "zod": "^4.1.12"
-  },
-  "devDependencies": {
-    "@d8d/shared-test-util": "workspace:*",
-    "typescript": "^5.8.3",
-    "vitest": "^3.2.4"
-  }
-}

+ 0 - 1
packages/unified-file-module/src/entities/index.ts

@@ -1 +0,0 @@
-export * from './unified-file.entity';

+ 0 - 118
packages/unified-file-module/src/entities/unified-file.entity.ts

@@ -1,118 +0,0 @@
-import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
-import process from 'node:process';
-import { MinioService } from '../services/minio.service';
-
-@Entity('unified_file')
-export class UnifiedFile {
-  @PrimaryGeneratedColumn({
-    name: 'id',
-    type: 'int',
-    unsigned: true,
-    comment: '文件ID'
-  })
-  id!: number;
-
-  @Column({
-    name: 'file_name',
-    type: 'varchar',
-    length: 255,
-    comment: '文件名'
-  })
-  fileName!: string;
-
-  @Column({
-    name: 'file_path',
-    type: 'varchar',
-    length: 512,
-    comment: '文件存储路径'
-  })
-  filePath!: string;
-
-  @Column({
-    name: 'file_size',
-    type: 'int',
-    unsigned: true,
-    nullable: true,
-    comment: '文件大小,单位字节'
-  })
-  fileSize!: number | null;
-
-  @Column({
-    name: 'mime_type',
-    type: 'varchar',
-    length: 100,
-    nullable: true,
-    comment: 'MIME类型'
-  })
-  mimeType!: string | null;
-
-  @Column({
-    name: 'description',
-    type: 'text',
-    nullable: true,
-    comment: '文件描述'
-  })
-  description!: string | null;
-
-  @Column({
-    name: 'created_by',
-    type: 'int',
-    unsigned: true,
-    nullable: true,
-    comment: '创建者ID'
-  })
-  createdBy!: number | null;
-
-  @Column({
-    name: 'updated_by',
-    type: 'int',
-    unsigned: true,
-    nullable: true,
-    comment: '更新者ID'
-  })
-  updatedBy!: number | null;
-
-  @Column({
-    name: 'status',
-    type: 'int',
-    default: 1,
-    comment: '状态: 1=启用, 0=禁用'
-  })
-  @Index('idx_unified_file_status')
-  status!: number;
-
-  @Column({
-    name: 'created_at',
-    type: 'timestamp',
-    default: () => 'CURRENT_TIMESTAMP',
-    comment: '创建时间'
-  })
-  @Index('idx_unified_file_created_at')
-  createdAt!: Date;
-
-  @Column({
-    name: 'updated_at',
-    type: 'timestamp',
-    default: () => 'CURRENT_TIMESTAMP',
-    onUpdate: 'CURRENT_TIMESTAMP',
-    comment: '更新时间'
-  })
-  updatedAt!: Date;
-
-  // 获取完整的文件URL(MinIO预签名URL)
-  get fullUrl(): Promise<string> {
-    const minioService = new MinioService();
-    const bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
-
-    return new Promise((resolve, reject) => {
-      minioService.getPresignedFileUrl(bucketName, this.filePath)
-        .then(url => {
-          resolve(url);
-        })
-        .catch(error => {
-          console.error('获取文件预签名URL失败:', error);
-          reject(error);
-        });
-    });
-  }
-}

+ 0 - 4
packages/unified-file-module/src/index.ts

@@ -1,4 +0,0 @@
-export * from "./entities";
-export * from "./services";
-export * from "./schemas";
-export * from "./routes";

+ 0 - 77
packages/unified-file-module/src/routes/[id]/delete.ts

@@ -1,77 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-
-// 删除文件路由
-const deleteFileRoute = createRoute({
-  method: 'delete',
-  path: '/{id}',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '文件删除成功',
-      content: {
-        'application/json': {
-          schema: z.object({
-            success: z.boolean().openapi({ example: true }),
-            message: z.string().openapi({ example: '文件删除成功' })
-          })
-        }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    404: {
-      description: '文件不存在',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-// 创建路由实例
-const app = new OpenAPIHono<AuthContext>().openapi(deleteFileRoute, async (c) => {
-  try {
-    const { id } = c.req.valid('param');
-
-    // 创建文件服务实例
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-    await unifiedFileService.deleteFile(id);
-
-    const validatedResponse = await parseWithAwait(
-      z.object({
-        success: z.boolean(),
-        message: z.string()
-      }),
-      { success: true, message: '文件删除成功' }
-    );
-    return c.json(validatedResponse, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '文件删除失败';
-    const code = (error instanceof Error && error.message === '文件不存在') ? 404 : 500;
-    return c.json({ code, message }, code);
-  }
-});
-
-export default app;

+ 0 - 83
packages/unified-file-module/src/routes/[id]/download.ts

@@ -1,83 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait } from '@d8d/shared-utils';function createZodErrorResponse(e: Error){return{code:400,message:"Validation failed"}};
-
-// 获取文件下载URL路由
-const downloadFileRoute = createRoute({
-  method: 'get',
-  path: '/{id}/download',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '获取文件下载URL成功',
-      content: {
-        'application/json': {
-          schema: z.object({
-            url: z.string().url().openapi({
-              description: '文件下载URL(带Content-Disposition头)',
-              example: 'https://minio.example.com/d8dai/file-key?response-content-disposition=attachment%3B%20filename%3D%22example.jpg%22'
-            }),
-            filename: z.string().openapi({
-              description: '原始文件名',
-              example: 'example.jpg'
-            })
-          })
-        }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    404: {
-      description: '文件不存在',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-// 创建路由实例
-const app = new OpenAPIHono<AuthContext>().openapi(downloadFileRoute, async (c) => {
-  try {
-    const { id } = c.req.valid('param');
-
-    // 创建文件服务实例
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-    const result = await unifiedFileService.getFileDownloadUrl(id);
-
-    const validatedResponse = await parseWithAwait(
-      z.object({
-        url: z.string().url(),
-        filename: z.string()
-      }),
-      result
-    );
-    return c.json(validatedResponse, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '获取文件下载URL失败';
-    const code = (error instanceof Error && error.message === '文件不存在') ? 404 : 500;
-    return c.json({ code, message }, code);
-  }
-});
-
-export default app;

+ 0 - 75
packages/unified-file-module/src/routes/[id]/get-url.ts

@@ -1,75 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-
-// 获取文件URL路由
-const getFileUrlRoute = createRoute({
-  method: 'get',
-  path: '/{id}/url',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '获取文件URL成功',
-      content: {
-        'application/json': {
-          schema: z.object({
-            url: z.string().url().openapi({
-              description: '文件访问URL',
-              example: 'https://minio.example.com/d8dai/file-key'
-            })
-          })
-        }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    404: {
-      description: '文件不存在',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-// 创建路由实例
-const app = new OpenAPIHono<AuthContext>().openapi(getFileUrlRoute, async (c) => {
-  try {
-    const { id } = c.req.valid('param');
-    // 创建文件服务实例
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-    const url = await unifiedFileService.getFileUrl(id);
-
-    const validatedResponse = await parseWithAwait(
-      z.object({ url: z.string().url() }),
-      { url }
-    );
-    return c.json(validatedResponse, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '获取文件URL失败';
-    const code = (error instanceof Error && error.message === '文件不存在') ? 404 : 500;
-    return c.json({ code, message }, code);
-  }
-});
-
-export default app;

+ 0 - 443
packages/unified-file-module/src/routes/admin/unified-files.admin.routes.ts

@@ -1,443 +0,0 @@
-import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
-import { AppDataSource, ErrorSchema, parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import {
-  UnifiedFileSchema,
-  CreateUnifiedFileDto,
-  UpdateUnifiedFileDto
-} from '../../schemas/unified-file.schema';
-
-interface AdminContext {
-  Variables: {
-    superAdminId?: number;
-    user?: any;
-    token?: string;
-    tenantId?: number;
-  };
-}
-
-const CommonErrorSchema = z.object({
-  code: z.number(),
-  message: z.string()
-});
-
-const getService = () => {
-  return new UnifiedFileService(AppDataSource);
-};
-
-const listRoute = createRoute({
-  method: 'get',
-  path: '/',
-  middleware: [tenantAuthMiddleware] as const,
-  request: {
-    query: z.object({
-      page: z.coerce.number<number>().int().positive().default(1).openapi({
-        example: 1,
-        description: '页码'
-      }),
-      pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
-        example: 10,
-        description: '每页数量'
-      }),
-      keyword: z.string().optional().openapi({
-        example: 'banner',
-        description: '搜索关键词'
-      }),
-      status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
-        example: 1,
-        description: '状态筛选'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '成功获取文件列表',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string(),
-            data: z.object({
-              list: z.array(UnifiedFileSchema),
-              total: z.number(),
-              page: z.number(),
-              pageSize: z.number()
-            })
-          })
-        }
-      }
-    },
-    400: {
-      description: '验证错误',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string(),
-            errors: z.array(z.object({
-              path: z.array(z.union([z.string(), z.number()])),
-              message: z.string(),
-              code: z.string()
-            })).optional()
-          })
-        }
-      }
-    },
-    500: {
-      description: '服务器错误',
-      content: {
-        'application/json': {
-          schema: CommonErrorSchema
-        }
-      }
-    }
-  }
-});
-
-const getRoute = createRoute({
-  method: 'get',
-  path: '/:id',
-  middleware: [tenantAuthMiddleware] as const,
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '成功获取文件详情',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string(),
-            data: UnifiedFileSchema
-          })
-        }
-      }
-    },
-    400: {
-      description: '验证错误',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string()
-          })
-        }
-      }
-    },
-    404: {
-      description: '文件不存在',
-      content: {
-        'application/json': {
-          schema: ErrorSchema
-        }
-      }
-    },
-    500: {
-      description: '服务器错误',
-      content: {
-        'application/json': {
-          schema: CommonErrorSchema
-        }
-      }
-    }
-  }
-});
-
-const createRouteDef = createRoute({
-  method: 'post',
-  path: '/',
-  middleware: [tenantAuthMiddleware] as const,
-  request: {
-    body: {
-      content: {
-        'application/json': {
-          schema: CreateUnifiedFileDto
-        }
-      }
-    }
-  },
-  responses: {
-    201: {
-      description: '成功创建文件',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string(),
-            data: UnifiedFileSchema
-          })
-        }
-      }
-    },
-    400: {
-      description: '验证错误',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string()
-          })
-        }
-      }
-    },
-    500: {
-      description: '服务器错误',
-      content: {
-        'application/json': {
-          schema: CommonErrorSchema
-        }
-      }
-    }
-  }
-});
-
-const updateRoute = createRoute({
-  method: 'put',
-  path: '/:id',
-  middleware: [tenantAuthMiddleware] as const,
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    }),
-    body: {
-      content: {
-        'application/json': {
-          schema: UpdateUnifiedFileDto
-        }
-      }
-    }
-  },
-  responses: {
-    200: {
-      description: '成功更新文件',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string(),
-            data: UnifiedFileSchema
-          })
-        }
-      }
-    },
-    400: {
-      description: '验证错误',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string()
-          })
-        }
-      }
-    },
-    404: {
-      description: '文件不存在',
-      content: {
-        'application/json': {
-          schema: ErrorSchema
-        }
-      }
-    },
-    500: {
-      description: '服务器错误',
-      content: {
-        'application/json': {
-          schema: CommonErrorSchema
-        }
-      }
-    }
-  }
-});
-
-const deleteRoute = createRoute({
-  method: 'delete',
-  path: '/:id',
-  middleware: [tenantAuthMiddleware] as const,
-  request: {
-    params: z.object({
-      id: z.coerce.number<number>().openapi({
-        param: { name: 'id', in: 'path' },
-        example: 1,
-        description: '文件ID'
-      })
-    })
-  },
-  responses: {
-    200: {
-      description: '成功删除文件',
-      content: {
-        'application/json': {
-          schema: z.object({
-            code: z.number(),
-            message: z.string()
-          })
-        }
-      }
-    },
-    404: {
-      description: '文件不存在',
-      content: {
-        'application/json': {
-          schema: ErrorSchema
-        }
-      }
-    },
-    500: {
-      description: '服务器错误',
-      content: {
-        'application/json': {
-          schema: CommonErrorSchema
-        }
-      }
-    }
-  }
-});
-
-const app = new OpenAPIHono<AdminContext>()
-  .openapi(listRoute, async (c) => {
-    try {
-      const query = c.req.valid('query');
-      const { page, pageSize, keyword, status } = query;
-      const service = getService();
-
-      const searchFields = ['name', 'description'];
-      const where = status !== undefined ? { status } : undefined;
-
-      const [list, total] = await service.getList(
-        page,
-        pageSize,
-        keyword,
-        searchFields,
-        where,
-        [],
-        { createdAt: 'DESC' }
-      );
-
-      const validatedList = await parseWithAwait(z.array(UnifiedFileSchema), list);
-
-      return c.json({
-        code: 200,
-        message: 'success',
-        data: {
-          list: validatedList,
-          total,
-          page,
-          pageSize
-        }
-      }, 200);
-    } catch (error) {
-      if ((error as Error).name === 'ZodError') {
-        return c.json(createZodErrorResponse(error as Error), 400);
-      }
-      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
-    }
-  })
-  .openapi(getRoute, async (c) => {
-    try {
-      const { id } = c.req.valid('param');
-      const service = getService();
-
-      const file = await service.getById(id);
-
-      if (!file) {
-        return c.json({ code: 404, message: 'File not found' }, 404);
-      }
-
-      const validatedData = await parseWithAwait(UnifiedFileSchema, file);
-
-      return c.json({
-        code: 200,
-        message: 'success',
-        data: validatedData
-      }, 200);
-    } catch (error) {
-      if ((error as Error).name === 'ZodError') {
-        return c.json(createZodErrorResponse(error as Error), 400);
-      }
-      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
-    }
-  })
-  .openapi(createRouteDef, async (c) => {
-    try {
-      const body = c.req.valid('json');
-      const superAdminId = c.get('superAdminId') || 1;
-      const service = getService();
-
-      const file = await service.create(body, superAdminId);
-
-      const validatedData = await parseWithAwait(UnifiedFileSchema, file);
-
-      return c.json({
-        code: 201,
-        message: 'File created successfully',
-        data: validatedData
-      }, 201);
-    } catch (error) {
-      if ((error as Error).name === 'ZodError') {
-        return c.json(createZodErrorResponse(error as Error), 400);
-      }
-      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
-    }
-  })
-  .openapi(updateRoute, async (c) => {
-    try {
-      const { id } = c.req.valid('param');
-      const body = c.req.valid('json');
-      const superAdminId = c.get('superAdminId') || 1;
-      const service = getService();
-
-      const file = await service.update(id, body, superAdminId);
-
-      if (!file) {
-        return c.json({ code: 404, message: 'File not found' }, 404);
-      }
-
-      const validatedData = await parseWithAwait(UnifiedFileSchema, file);
-
-      return c.json({
-        code: 200,
-        message: 'File updated successfully',
-        data: validatedData
-      }, 200);
-    } catch (error) {
-      if ((error as Error).name === 'ZodError') {
-        return c.json(createZodErrorResponse(error as Error), 400);
-      }
-      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
-    }
-  })
-  .openapi(deleteRoute, async (c) => {
-    try {
-      const { id } = c.req.valid('param');
-      const superAdminId = c.get('superAdminId') || 1;
-      const service = getService();
-
-      const success = await service.delete(id, superAdminId);
-
-      if (!success) {
-        return c.json({ code: 404, message: 'File not found' }, 404);
-      }
-
-      return c.json({
-        code: 200,
-        message: 'File deleted successfully'
-      }, 200);
-    } catch (error) {
-      return c.json({ code: 500, message: error instanceof Error ? error.message : 'Internal Server Error' }, 500);
-    }
-  });
-
-export default app;

+ 0 - 37
packages/unified-file-module/src/routes/index.ts

@@ -1,37 +0,0 @@
-import { OpenAPIHono } from '@hono/zod-openapi';
-import uploadPolicyRoute from './upload-policy/post';
-import multipartPolicyRoute from './multipart-policy/post';
-import completeMultipartRoute from './multipart-complete/post';
-import getUrlRoute from './[id]/get-url';
-import deleteRoute from './[id]/delete';
-import downloadRoute from './[id]/download';
-import { AuthContext } from '@d8d/shared-types';
-
-import { createCrudRoutes } from '@d8d/shared-crud';
-import { UnifiedFile } from '../entities/unified-file.entity';
-import { UnifiedFileSchema, CreateUnifiedFileDto, UpdateUnifiedFileDto } from '../schemas/unified-file.schema';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-
-const unifiedFileCrudRoutes = createCrudRoutes({
-  entity: UnifiedFile,
-  createSchema: CreateUnifiedFileDto,
-  updateSchema: UpdateUnifiedFileDto,
-  getSchema: UnifiedFileSchema,
-  listSchema: UnifiedFileSchema,
-  searchFields: ['fileName', 'mimeType', 'description'],
-  relations: [],
-  middleware: [tenantAuthMiddleware]
-});
-
-// 创建路由实例并聚合所有子路由
-const unifiedFileRoutes = new OpenAPIHono<AuthContext>()
-  .route('/upload-policy', uploadPolicyRoute)
-  .route('/multipart-policy', multipartPolicyRoute)
-  .route('/multipart-complete', completeMultipartRoute)
-  .route('/', getUrlRoute)
-  .route('/', downloadRoute)
-  .route('/', deleteRoute)
-  .route('/', unifiedFileCrudRoutes);
-
-export { unifiedFileRoutes };
-export default unifiedFileRoutes;

+ 0 - 142
packages/unified-file-module/src/routes/multipart-complete/post.ts

@@ -1,142 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-
-// 完成分片上传请求Schema
-const CompleteMultipartUploadDto = z.object({
-  uploadId: z.string().openapi({
-    description: '分片上传ID',
-    example: '123e4567-e89b-12d3-a456-426614174000'
-  }),
-  bucket: z.string().openapi({
-    description: '存储桶名称',
-    example: 'd8dai'
-  }),
-  key: z.string().openapi({
-    description: '文件键名',
-    example: 'documents/report.pdf'
-  }),
-  parts: z.array(
-    z.object({
-      partNumber: z.coerce.number<number>().int().positive().openapi({
-        description: '分片序号',
-        example: 1
-      }),
-      etag: z.string().openapi({
-        description: '分片ETag值',
-        example: 'd41d8cd98f00b204e9800998ecf8427e'
-      })
-    })
-  ).openapi({
-    description: '分片信息列表',
-    example: [
-      { partNumber: 1, etag: 'd41d8cd98f00b204e9800998ecf8427e' },
-      { partNumber: 2, etag: '5f4dcc3b5aa765d61d8327deb882cf99' }
-    ]
-  })
-});
-
-// 完成分片上传响应Schema
-const CompleteMultipartUploadResponse = z.object({
-  fileId: z.number().openapi({
-    description: '文件ID',
-    example: 123456
-  }),
-  url: z.string().url().openapi({
-    description: '文件访问URL',
-    example: 'https://minio.example.com/d8dai/documents/report.pdf'
-  }),
-  host: z.string().openapi({
-    description: 'MinIO主机地址',
-    example: 'minio.example.com'
-  }),
-  bucket: z.string().openapi({
-    description: '存储桶名称',
-    example: 'd8dai'
-  }),
-  key: z.string().openapi({
-    description: '文件键名',
-    example: 'documents/report.pdf'
-  }),
-  size: z.number().openapi({
-    description: '文件大小(字节)',
-    example: 102400
-  })
-});
-
-// 创建完成分片上传路由定义
-const completeMultipartUploadRoute = createRoute({
-  method: 'post',
-  path: '/',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    body: {
-      content: {
-        'application/json': { schema: CompleteMultipartUploadDto }
-      }
-    }
-  },
-  responses: {
-    200: {
-      description: '完成分片上传成功',
-      content: {
-        'application/json': { schema: CompleteMultipartUploadResponse }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    400: {
-      description: '请求参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-// 创建路由实例并实现处理逻辑
-const app = new OpenAPIHono<AuthContext>().openapi(completeMultipartUploadRoute, async (c) => {
-  try {
-    const data = await c.req.json();
-
-    // 初始化UnifiedFileService
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-    const result = await unifiedFileService.completeMultipartUpload(data);
-
-    // 构建完整的响应包含host和bucket信息
-    const response = {
-      ...result,
-      host: `${process.env.MINIO_USE_SSL ? 'https' : 'http'}://${process.env.MINIO_ENDPOINT || process.env.MINIO_HOST}:${process.env.MINIO_PORT}`,
-      bucket: data.bucket
-    };
-
-    const validatedResponse = await parseWithAwait(
-      z.object({
-        fileId: z.number(),
-        url: z.string().url(),
-        host: z.string(),
-        bucket: z.string(),
-        key: z.string(),
-        size: z.number()
-      }),
-      response
-    );
-    return c.json(validatedResponse, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '完成分片上传失败';
-    return c.json({ code: 500, message }, 500);
-  }
-});
-
-export default app;

+ 0 - 138
packages/unified-file-module/src/routes/multipart-policy/post.ts

@@ -1,138 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-
-// 创建分片上传策略请求Schema
-const CreateMultipartUploadPolicyDto = z.object({
-fileKey: z.string().openapi({
-  description: '文件键名',
-  example: 'documents/report.pdf'
-}),
-totalSize: z.coerce.number<number>().int().positive().openapi({
-  description: '文件总大小(字节)',
-  example: 10485760
-}),
-partSize: z.coerce.number<number>().int().positive().openapi({
-  description: '分片大小(字节)',
-  example: 5242880
-}),
-mimeType: z.string().max(100).nullable().optional().openapi({
-  description: '文件类型',
-  example: 'application/pdf'
-}),
-fileName: z.string().max(255).openapi({
-  description: '文件名称',
-  example: '项目计划书.pdf'
-})
-});
-
-// 创建分片上传策略路由定义
-const createMultipartUploadPolicyRoute = createRoute({
-  method: 'post',
-  path: '/',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    body: {
-      content: {
-        'application/json': { schema: CreateMultipartUploadPolicyDto }
-      }
-    }
-  },
-  responses: {
-    200: {
-      description: '生成分片上传策略成功',
-      content: {
-        'application/json': {
-          schema: z.object({
-            uploadId: z.string().openapi({
-              description: '分片上传ID',
-              example: '123e4567-e89b-12d3-a456-426614174000'
-            }),
-            bucket: z.string().openapi({
-              description: '存储桶名称',
-              example: 'd8dai'
-            }),
-            key: z.string().openapi({
-              description: '文件键名',
-              example: 'documents/report.pdf'
-            }),
-            host: z.string().openapi({
-              description: 'MinIO主机地址',
-              example: 'minio.example.com'
-            }),
-            partUrls: z.array(z.string()).openapi({
-              description: '分片上传URL列表',
-              example: [
-                'https://minio.example.com/d8dai/documents/report.pdf?uploadId=123e4567-e89b-12d3-a456-426614174000&partNumber=1',
-                'https://minio.example.com/d8dai/documents/report.pdf?uploadId=123e4567-e89b-12d3-a456-426614174000&partNumber=2'
-              ]
-            })
-          })
-        }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    400: {
-      description: '请求参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-
-// 创建路由实例
-const app = new OpenAPIHono<AuthContext>().openapi(createMultipartUploadPolicyRoute, async (c) => {
-  try {
-    const data = await c.req.json();
-    const user = c.get('user');
-
-    // 计算分片数量
-    const partCount = Math.ceil(data.totalSize / data.partSize);
-
-    // 创建文件服务实例
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-    const result = await unifiedFileService.createMultipartUploadPolicy({
-      ...data,
-      createdBy: user.id
-    }, partCount);
-
-    const response = {
-      uploadId: result.uploadId,
-      bucket: result.bucket,
-      key: result.key,
-      host: `${process.env.MINIO_USE_SSL ? 'https' : 'http'}://${process.env.MINIO_ENDPOINT || process.env.MINIO_HOST}:${process.env.MINIO_PORT}`,
-      partUrls: result.uploadUrls
-    };
-
-    const validatedResponse = await parseWithAwait(
-      z.object({
-        uploadId: z.string(),
-        bucket: z.string(),
-        key: z.string(),
-        host: z.string(),
-        partUrls: z.array(z.string())
-      }),
-      response
-    );
-    return c.json(validatedResponse, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '生成分片上传策略失败';
-    return c.json({ code: 500, message }, 500);
-  }
-});
-
-export default app;

+ 0 - 89
packages/unified-file-module/src/routes/upload-policy/post.ts

@@ -1,89 +0,0 @@
-import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
-import { UnifiedFileService } from '../../services/unified-file.service';
-import { UnifiedFileSchema, CreateUnifiedFileDto } from '../../schemas/unified-file.schema';
-import { ErrorSchema } from '@d8d/shared-utils';
-import { AppDataSource } from '@d8d/shared-utils';
-import { AuthContext } from '@d8d/shared-types';
-import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
-import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
-
-const CreateUnifiedFileResponseSchema = z.object({
-  file: UnifiedFileSchema,
-  uploadPolicy: z.object({
-    'x-amz-algorithm': z.string(),
-    'x-amz-credential': z.string(),
-    'x-amz-date': z.string(),
-    'x-amz-security-token': z.string().optional(),
-    policy: z.string(),
-    'x-amz-signature': z.string(),
-    host: z.string(),
-    key: z.string(),
-    bucket: z.string()
-  })
-});
-
-// 创建文件上传策略路由
-const createUploadPolicyRoute = createRoute({
-  method: 'post',
-  path: '/',
-  middleware: [tenantAuthMiddleware],
-  request: {
-    body: {
-      content: {
-        'application/json': { schema: CreateUnifiedFileDto }
-      }
-    }
-  },
-  responses: {
-    200: {
-      description: '生成文件上传策略成功',
-      content: {
-        'application/json': {
-          schema: CreateUnifiedFileResponseSchema
-        }
-      }
-    },
-    401: {
-      description: '未授权访问',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    400: {
-      description: '请求参数错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    },
-    500: {
-      description: '服务器错误',
-      content: { 'application/json': { schema: ErrorSchema } }
-    }
-  }
-});
-
-// 创建路由实例
-const app = new OpenAPIHono<AuthContext>().openapi(createUploadPolicyRoute, async (c) => {
-  try {
-    const data = c.req.valid('json');
-    const user = c.get('user');
-
-    // 创建文件服务实例
-    const unifiedFileService = new UnifiedFileService(AppDataSource);
-
-    // 添加用户ID到文件数据
-    const fileData = {
-      ...data,
-      createdBy: user.id
-    };
-    const result = await unifiedFileService.createFile(fileData);
-
-    // 使用 parseWithAwait 验证响应数据
-    const validatedResult = await parseWithAwait(CreateUnifiedFileResponseSchema, result);
-    return c.json(validatedResult, 200);
-  } catch (error) {
-    if (error instanceof z.ZodError) {
-      return c.json(createZodErrorResponse(error), 400);
-    }
-    const message = error instanceof Error ? error.message : '生成上传策略失败';
-    return c.json({ code: 500, message }, 500);
-  }
-});
-
-export default app;

+ 0 - 1
packages/unified-file-module/src/schemas/index.ts

@@ -1 +0,0 @@
-export * from "./unified-file.schema";

+ 0 - 101
packages/unified-file-module/src/schemas/unified-file.schema.ts

@@ -1,101 +0,0 @@
-import { z } from '@hono/zod-openapi';
-
-export const UnifiedFileSchema = z.object({
-  id: z.number().int().positive().openapi({
-    description: '文件ID',
-    example: 1
-  }),
-  fileName: z.string().max(255).openapi({
-    description: '文件名称',
-    example: 'banner.jpg'
-  }),
-  filePath: z.string().max(512).openapi({
-    description: '文件存储路径',
-    example: 'unified/uuid-banner.jpg'
-  }),
-  fileSize: z.number().int().positive().nullable().openapi({
-    description: '文件大小(字节)',
-    example: 102400
-  }),
-  mimeType: z.string().max(100).nullable().openapi({
-    description: 'MIME类型',
-    example: 'image/jpeg'
-  }),
-  description: z.string().nullable().openapi({
-    description: '文件描述',
-    example: '首页轮播图'
-  }),
-  createdBy: z.number().int().positive().nullable().openapi({
-    description: '创建者ID',
-    example: 1
-  }),
-  updatedBy: z.number().int().positive().nullable().openapi({
-    description: '更新者ID',
-    example: 1
-  }),
-  status: z.number().int().openapi({
-    description: '状态: 1=启用, 0=禁用',
-    example: 1
-  }),
-  createdAt: z.coerce.date<Date>().openapi({
-    description: '创建时间',
-    example: '2024-01-15T10:30:00Z'
-  }),
-  updatedAt: z.coerce.date<Date>().openapi({
-    description: '更新时间',
-    example: '2024-01-16T14:20:00Z'
-  })
-});
-
-export const CreateUnifiedFileDto = z.object({
-  fileName: z.string().min(1).max(255).openapi({
-    description: '文件名称',
-    example: 'banner.jpg'
-  }),
-  filePath: z.string().max(512).openapi({
-    description: '文件存储路径',
-    example: 'unified/uuid-banner.jpg'
-  }),
-  fileSize: z.coerce.number<number>().int().positive().optional().openapi({
-    description: '文件大小(字节)',
-    example: 102400
-  }),
-  mimeType: z.string().max(100).optional().openapi({
-    description: 'MIME类型',
-    example: 'image/jpeg'
-  }),
-  description: z.string().optional().openapi({
-    description: '文件描述',
-    example: '首页轮播图'
-  }),
-  createdBy: z.number().int().positive().optional().openapi({
-    description: '创建者ID',
-    example: 1
-  })
-});
-
-export const UpdateUnifiedFileDto = z.object({
-  fileName: z.string().max(255).optional().openapi({
-    description: '文件名称',
-    example: 'banner_v2.jpg'
-  }),
-  description: z.string().optional().openapi({
-    description: '文件描述',
-    example: '首页轮播图(更新版)'
-  }),
-  status: z.number().int().min(0).max(1).optional().openapi({
-    description: '状态: 1=启用, 0=禁用',
-    example: 1
-  })
-});
-
-export const UnifiedFileListResponseSchema = z.object({
-  code: z.number(),
-  message: z.string(),
-  data: z.object({
-    list: z.array(UnifiedFileSchema),
-    total: z.number(),
-    page: z.number(),
-    pageSize: z.number()
-  })
-});

+ 0 - 2
packages/unified-file-module/src/services/index.ts

@@ -1,2 +0,0 @@
-export * from './unified-file.service';
-export * from './minio.service';

+ 0 - 200
packages/unified-file-module/src/services/minio.service.ts

@@ -1,200 +0,0 @@
-import { Client } from 'minio';
-import { logger } from '@d8d/shared-utils';
-import * as process from 'node:process';
-
-export class MinioService {
-  private readonly client: Client;
-  public readonly bucketName: string;
-
-  constructor() {
-    this.client = new Client({
-      endPoint: process.env.MINIO_HOST || 'localhost',
-      port: parseInt(process.env.MINIO_PORT || '443'),
-      useSSL: process.env.MINIO_USE_SSL !== 'false',
-      accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
-      secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
-    });
-    this.bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
-  }
-
-  async ensureBucketExists(bucketName: string = this.bucketName) {
-    try {
-      const exists = await this.client.bucketExists(bucketName);
-      if (!exists) {
-        await this.client.makeBucket(bucketName);
-        logger.db(`Created new bucket: ${bucketName}`);
-      }
-      return true;
-    } catch (error) {
-      logger.error(`Failed to ensure bucket exists: ${bucketName}`, error);
-      throw error;
-    }
-  }
-
-  async generateUploadPolicy(fileKey: string) {
-    await this.ensureBucketExists();
-    const expiresAt = new Date(Date.now() + 3600 * 1000);
-    const policy = this.client.newPostPolicy();
-    policy.setBucket(this.bucketName);
-    policy.setKey(fileKey);
-    policy.setExpires(expiresAt);
-
-    const { postURL, formData } = await this.client.presignedPostPolicy(policy);
-
-    return {
-      'x-amz-algorithm': formData['x-amz-algorithm'],
-      'x-amz-credential': formData['x-amz-credential'],
-      'x-amz-date': formData['x-amz-date'],
-      'x-amz-security-token': formData['x-amz-security-token'] || undefined,
-      policy: formData['policy'],
-      'x-amz-signature': formData['x-amz-signature'],
-      host: postURL,
-      key: fileKey,
-      bucket: this.bucketName,
-    };
-  }
-
-  getFileUrl(bucketName: string, fileKey: string) {
-    const protocol = process.env.MINIO_USE_SSL !== 'false' ? 'https' : 'http';
-    const port = process.env.MINIO_PORT ? `:${process.env.MINIO_PORT}` : '';
-    return `${protocol}://${process.env.MINIO_HOST}${port}/${bucketName}/${fileKey}`;
-  }
-
-  async getPresignedFileUrl(bucketName: string, fileKey: string, expiresInSeconds = 3600) {
-    try {
-      const url = await this.client.presignedGetObject(bucketName, fileKey, expiresInSeconds);
-      logger.db(`Generated presigned URL for ${bucketName}/${fileKey}`);
-      return url;
-    } catch (error) {
-      logger.error(`Failed to generate presigned URL:`, error);
-      throw error;
-    }
-  }
-
-  async getPresignedFileDownloadUrl(bucketName: string, fileKey: string, filename: string, expiresInSeconds = 3600) {
-    try {
-      const url = await this.client.presignedGetObject(
-        bucketName,
-        fileKey,
-        expiresInSeconds,
-        {
-          'response-content-disposition': `attachment; filename="${encodeURIComponent(filename)}"`,
-          'response-content-type': 'application/octet-stream'
-        }
-      );
-      return url;
-    } catch (error) {
-      logger.error(`Failed to generate presigned download URL:`, error);
-      throw error;
-    }
-  }
-
-  async createObject(bucketName: string, objectName: string, fileContent: Buffer, contentType: string = 'application/octet-stream') {
-    try {
-      await this.ensureBucketExists(bucketName);
-      await this.client.putObject(bucketName, objectName, fileContent, fileContent.length, {
-        'Content-Type': contentType
-      });
-      logger.db(`Created object: ${bucketName}/${objectName}`);
-      return this.getFileUrl(bucketName, objectName);
-    } catch (error) {
-      logger.error(`Failed to create object:`, error);
-      throw error;
-    }
-  }
-
-  async objectExists(bucketName: string, objectName: string): Promise<boolean> {
-    try {
-      await this.client.statObject(bucketName, objectName);
-      return true;
-    } catch (error) {
-      if ((error as Error).message.includes('not found')) {
-        return false;
-      }
-      throw error;
-    }
-  }
-
-  async deleteObject(bucketName: string, objectName: string) {
-    try {
-      await this.client.removeObject(bucketName, objectName);
-      logger.db(`Deleted object: ${bucketName}/${objectName}`);
-    } catch (error) {
-      logger.error(`Failed to delete object:`, error);
-      throw error;
-    }
-  }
-
-  /**
-   * 创建分段上传会话
-   */
-  async createMultipartUpload(bucketName: string, objectName: string) {
-    try {
-      const uploadId = await this.client.initiateNewMultipartUpload(bucketName, objectName, {});
-      logger.db(`Created multipart upload for ${objectName} with ID: ${uploadId}`);
-      return uploadId;
-    } catch (error) {
-      logger.error(`Failed to create multipart upload for ${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  /**
-   * 生成分段上传预签名URL
-   */
-  async generateMultipartUploadUrls(
-    bucketName: string,
-    objectName: string,
-    uploadId: string,
-    partCount: number,
-    expiresInSeconds = 3600
-  ) {
-    try {
-      const partUrls = [];
-      for (let partNumber = 1; partNumber <= partCount; partNumber++) {
-        const url = await this.client.presignedUrl(
-          'put',
-          bucketName,
-          objectName,
-          expiresInSeconds,
-          {
-            uploadId,
-            partNumber: partNumber.toString()
-          }
-        );
-        partUrls.push(url);
-      }
-      return partUrls;
-    } catch (error) {
-      logger.error(`Failed to generate multipart upload URLs for ${objectName}:`, error);
-      throw error;
-    }
-  }
-
-  /**
-   * 完成分段上传
-   */
-  async completeMultipartUpload(
-    bucketName: string,
-    objectName: string,
-    uploadId: string,
-    parts: { ETag: string; PartNumber: number }[]
-  ): Promise<{ size: number }> {
-    try {
-      await this.client.completeMultipartUpload(
-        bucketName,
-        objectName,
-        uploadId,
-        parts.map(p => ({ part: p.PartNumber, etag: p.ETag }))
-      );
-      logger.db(`Completed multipart upload for ${objectName} with ID: ${uploadId}`);
-
-      // 获取对象信息以获取文件大小
-      const stat = await this.client.statObject(bucketName, objectName);
-      return { size: stat.size };
-    } catch (error) {
-      logger.error(`Failed to complete multipart upload for ${objectName}:`, error);
-      throw error;
-    }
-  }
-}

+ 0 - 463
packages/unified-file-module/src/services/unified-file.service.ts

@@ -1,463 +0,0 @@
-import { GenericCrudService } from '@d8d/shared-crud';
-import { DataSource } from 'typeorm';
-import { UnifiedFile } from '../entities/unified-file.entity';
-import { MinioService } from './minio.service';
-import { v4 as uuidv4 } from 'uuid';
-import { logger } from '@d8d/shared-utils';
-
-export class UnifiedFileService extends GenericCrudService<UnifiedFile> {
-  private readonly minioService: MinioService;
-
-  constructor(dataSource: DataSource) {
-    super(dataSource, UnifiedFile);
-    this.minioService = new MinioService();
-  }
-
-  /**
-   * 覆盖创建方法,设置默认值
-   */
-  override async create(data: Partial<UnifiedFile>, userId?: string | number): Promise<UnifiedFile> {
-    const fileData = {
-      ...data,
-      status: data.status ?? 1,
-      createdAt: new Date(),
-      updatedAt: new Date()
-    };
-
-    return super.create(fileData, userId);
-  }
-
-  /**
-   * 覆盖更新方法,自动设置 updatedAt
-   */
-  override async update(id: number, data: Partial<UnifiedFile>, userId?: string | number): Promise<UnifiedFile | null> {
-    const updateData = {
-      ...data,
-      updatedAt: new Date()
-    };
-
-    return super.update(id, updateData, userId);
-  }
-
-  /**
-   * 覆盖删除方法,实现软删除
-   */
-  override async delete(id: number, userId?: string | number): Promise<boolean> {
-    const file = await this.getById(id);
-    if (!file) {
-      return false;
-    }
-
-    // 软删除:设置 status = 0
-    await this.update(id, { status: 0 }, userId);
-    return true;
-  }
-
-  /**
-   * 创建文件记录并生成预签名上传URL
-   */
-  async createFile(data: Partial<UnifiedFile>) {
-    try {
-      // 生成唯一文件存储路径
-      const fileKey = `unified/${uuidv4()}-${data.fileName}`;
-      // 生成MinIO上传策略
-      const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey);
-
-      // 准备文件记录数据
-      const fileData = {
-        ...data,
-        filePath: fileKey,
-        status: 1,
-        createdAt: new Date(),
-        updatedAt: new Date()
-      };
-
-      // 保存文件记录到数据库
-      const savedFile = await this.create(fileData as UnifiedFile);
-
-      // 返回文件记录和上传策略
-      return {
-        file: savedFile,
-        uploadPolicy
-      };
-    } catch (error) {
-      logger.error('Failed to create file:', error);
-      throw new Error('文件创建失败');
-    }
-  }
-
-  /**
-   * 删除文件记录及对应的MinIO文件
-   */
-  async deleteFile(id: number) {
-    // 获取文件记录
-    const file = await this.getById(id);
-    if (!file) {
-      throw new Error('文件不存在');
-    }
-
-    try {
-      // 验证文件是否存在于MinIO
-      const fileExists = await this.minioService.objectExists(this.minioService.bucketName, file.filePath);
-      if (!fileExists) {
-        logger.error(`File not found in MinIO: ${this.minioService.bucketName}/${file.filePath}`);
-      } else {
-        // 从MinIO删除文件
-        await this.minioService.deleteObject(this.minioService.bucketName, file.filePath);
-      }
-
-      // 软删除数据库记录
-      await this.delete(id);
-
-      return true;
-    } catch (error) {
-      logger.error('Failed to delete file:', error);
-      throw new Error('文件删除失败');
-    }
-  }
-
-  /**
-   * 获取文件访问URL
-   */
-  async getFileUrl(id: number) {
-    const file = await this.getById(id);
-    if (!file) {
-      throw new Error('文件不存在');
-    }
-
-    return this.minioService.getPresignedFileUrl(this.minioService.bucketName, file.filePath);
-  }
-
-  /**
-   * 获取文件下载URL(带Content-Disposition头)
-   */
-  async getFileDownloadUrl(id: number) {
-    const file = await this.getById(id);
-    if (!file) {
-      throw new Error('文件不存在');
-    }
-
-    const url = await this.minioService.getPresignedFileDownloadUrl(
-      this.minioService.bucketName,
-      file.filePath,
-      file.fileName
-    );
-
-    return {
-      url,
-      filename: file.fileName
-    };
-  }
-
-  /**
-   * 创建多部分上传策略
-   */
-  async createMultipartUploadPolicy(data: Partial<UnifiedFile>, partCount: number) {
-    try {
-      // 生成唯一文件存储路径
-      const fileKey = `unified/${uuidv4()}-${data.fileName}`;
-
-      // 初始化多部分上传
-      const uploadId = await this.minioService.createMultipartUpload(
-        this.minioService.bucketName,
-        fileKey
-      );
-
-      // 生成各部分上传URL
-      const uploadUrls = await this.minioService.generateMultipartUploadUrls(
-        this.minioService.bucketName,
-        fileKey,
-        uploadId,
-        partCount
-      );
-
-      // 准备文件记录数据
-      const fileData = {
-        ...data,
-        filePath: fileKey,
-        status: 1,
-        createdAt: new Date(),
-        updatedAt: new Date()
-      };
-
-      // 保存文件记录到数据库
-      const savedFile = await this.create(fileData as UnifiedFile);
-
-      // 返回文件记录和上传策略
-      return {
-        file: savedFile,
-        uploadId,
-        uploadUrls,
-        bucket: this.minioService.bucketName,
-        key: fileKey
-      };
-    } catch (error) {
-      logger.error('Failed to create multipart upload policy:', error);
-      throw new Error('创建多部分上传策略失败');
-    }
-  }
-
-  /**
-   * 完成分片上传
-   */
-  async completeMultipartUpload(data: {
-    uploadId: string;
-    bucket: string;
-    key: string;
-    parts: Array<{ partNumber: number; etag: string }>;
-  }) {
-    logger.db('Starting multipart upload completion:', {
-      uploadId: data.uploadId,
-      bucket: data.bucket,
-      key: data.key,
-      partsCount: data.parts.length
-    });
-
-    // 查找文件记录
-    const file = await this.repository.findOneBy({ filePath: data.key });
-    if (!file) {
-      throw new Error('文件记录不存在');
-    }
-
-    try {
-      // 完成MinIO分片上传 - 注意格式转换
-      const result = await this.minioService.completeMultipartUpload(
-        data.bucket,
-        data.key,
-        data.uploadId,
-        data.parts.map(part => ({ PartNumber: part.partNumber, ETag: part.etag }))
-      );
-
-      // 更新文件大小等信息
-      file.fileSize = result.size;
-      file.updatedAt = new Date();
-      await this.repository.save(file);
-
-      // 生成文件访问URL
-      const url = this.minioService.getFileUrl(data.bucket, data.key);
-
-      logger.db('Multipart upload completed successfully:', {
-        fileId: file.id,
-        size: result.size,
-        key: data.key
-      });
-
-      return {
-        fileId: file.id,
-        url,
-        key: data.key,
-        size: result.size
-      };
-    } catch (error) {
-      logger.error('Failed to complete multipart upload:', error);
-      throw new Error('完成分片上传失败');
-    }
-  }
-
-  /**
-   * 保存文件记录并将文件内容直接上传到MinIO(支持自定义存储路径)
-   */
-  async saveFileWithCustomPath(
-    fileData: {
-      fileName: string;
-      fileSize: number;
-      mimeType: string;
-      createdBy?: number;
-      [key: string]: any;
-    },
-    fileContent: Buffer,
-    customPath?: string,
-    contentType?: string
-  ) {
-    try {
-      logger.db('Starting saveFileWithCustomPath process:', {
-        filename: fileData.fileName,
-        size: fileData.fileSize,
-        mimeType: fileData.mimeType,
-        customPath: customPath || 'auto-generated'
-      });
-
-      // 使用自定义路径或生成唯一文件存储路径
-      const fileKey = customPath || `unified/${uuidv4()}-${fileData.fileName}`;
-
-      // 确保存储桶存在
-      await this.minioService.ensureBucketExists();
-
-      // 直接上传文件内容到MinIO
-      const fileUrl = await this.minioService.createObject(
-        this.minioService.bucketName,
-        fileKey,
-        fileContent,
-        contentType || fileData.mimeType
-      );
-
-      // 准备文件记录数据
-      const completeFileData = {
-        ...fileData,
-        filePath: fileKey,
-        status: 1
-      };
-
-      // 保存文件记录到数据库
-      const savedFile = await this.create(completeFileData as any);
-
-      logger.db('File saved with custom path successfully:', {
-        fileId: savedFile.id,
-        filename: savedFile.fileName,
-        size: savedFile.fileSize,
-        path: fileKey,
-        url: fileUrl
-      });
-
-      return {
-        file: savedFile,
-        url: fileUrl
-      };
-    } catch (error) {
-      logger.error('Failed to save file with custom path:', error);
-      throw new Error(`文件保存失败: ${error instanceof Error ? error.message : '未知错误'}`);
-    }
-  }
-
-  /**
-   * 从URL下载文件并保存到MinIO
-   */
-  async downloadAndSaveFromUrl(
-    url: string,
-    fileData: {
-      createdBy?: number;
-      mimeType?: string;
-      customFileName?: string;
-      customPath?: string;
-      [key: string]: any;
-    },
-    options?: {
-      timeout?: number;
-      retries?: number;
-    }
-  ) {
-    try {
-      const axios = require('axios');
-
-      logger.db('Starting downloadAndSaveFromUrl process:', {
-        url,
-        customFileName: fileData.customFileName,
-        customPath: fileData.customPath
-      });
-
-      // 下载文件
-      const response = await axios.get(url, {
-        responseType: 'arraybuffer',
-        timeout: options?.timeout || 30000,
-        maxRedirects: 5,
-        headers: {
-          'User-Agent': 'Mozilla/5.0 (compatible; FileDownloader/1.0)'
-        }
-      });
-
-      const buffer = Buffer.from(response.data);
-
-      // 从URL或响应头中获取文件名
-      let fileName = fileData.customFileName;
-      if (!fileName) {
-        const contentDisposition = response.headers['content-disposition'];
-        if (contentDisposition) {
-          const filenameMatch = contentDisposition.match(/filename[*]?=(?:utf-8'')?(.+)/i);
-          if (filenameMatch) {
-            fileName = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
-          }
-        }
-
-        if (!fileName) {
-          const urlPath = new URL(url).pathname;
-          fileName = urlPath.split('/').pop() || `file_${Date.now()}`;
-        }
-      }
-
-      // 确保文件有扩展名
-      if (!fileName.includes('.') && fileData.mimeType) {
-        const ext = this.getExtensionFromMimeType(fileData.mimeType);
-        if (ext) {
-          fileName += `.${ext}`;
-        }
-      }
-
-      // 确定MIME类型
-      let mimeType = fileData.mimeType || response.headers['content-type'];
-      if (!mimeType || mimeType === 'application/octet-stream') {
-        mimeType = this.inferMimeType(fileName);
-      }
-
-      // 保存文件
-      const saveResult = await this.saveFileWithCustomPath(
-        {
-          ...fileData,
-          fileName,
-          fileSize: buffer.length,
-          mimeType
-        },
-        buffer,
-        fileData.customPath,
-        mimeType
-      );
-
-      logger.db('Download and save completed successfully:', {
-        fileId: saveResult.file.id,
-        fileName,
-        size: buffer.length,
-        url: saveResult.url
-      });
-
-      return saveResult;
-    } catch (error) {
-      logger.error('Failed to download and save file from URL:', {
-        url,
-        error: error instanceof Error ? error.message : '未知错误'
-      });
-      throw new Error(`从URL下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
-    }
-  }
-
-  /**
-   * 根据MIME类型获取文件扩展名
-   */
-  private getExtensionFromMimeType(mimeType: string): string | null {
-    const mimeMap: Record<string, string> = {
-      'image/jpeg': 'jpg',
-      'image/png': 'png',
-      'image/gif': 'gif',
-      'image/webp': 'webp',
-      'image/svg+xml': 'svg',
-      'application/pdf': 'pdf',
-      'text/plain': 'txt',
-      'application/json': 'json',
-      'application/xml': 'xml',
-      'video/mp4': 'mp4',
-      'audio/mp3': 'mp3'
-    };
-    return mimeMap[mimeType] || null;
-  }
-
-  /**
-   * 根据文件名推断MIME类型
-   */
-  private inferMimeType(fileName: string): string {
-    const ext = fileName.toLowerCase().split('.').pop();
-    const extMap: Record<string, string> = {
-      'jpg': 'image/jpeg',
-      'jpeg': 'image/jpeg',
-      'png': 'image/png',
-      'gif': 'image/gif',
-      'webp': 'image/webp',
-      'svg': 'image/svg+xml',
-      'pdf': 'application/pdf',
-      'txt': 'text/plain',
-      'json': 'application/json',
-      'xml': 'application/xml',
-      'mp4': 'video/mp4',
-      'mp3': 'audio/mp3',
-      'wav': 'audio/wav'
-    };
-    return extMap[ext || ''] || 'application/octet-stream';
-  }
-}

+ 0 - 154
packages/unified-file-module/tests/integration/unified-files.integration.test.ts

@@ -1,154 +0,0 @@
-import { describe, it, expect, beforeEach } from 'vitest';
-import { testClient } from 'hono/testing';
-import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
-import { JWTUtil } from '@d8d/shared-utils';
-import unifiedFilesAdminRoutes from '../../src/routes/admin/unified-files.admin.routes';
-import { UnifiedFile } from '../../src/entities/unified-file.entity';
-
-// 设置集成测试钩子 - 只需要 UnifiedFile 实体
-setupIntegrationDatabaseHooksWithEntities([UnifiedFile]);
-
-describe('统一文件模块集成测试', () => {
-  describe('管理员路由(超级管理员专用)', () => {
-    let adminClient: ReturnType<typeof testClient<typeof unifiedFilesAdminRoutes>>;
-    let superAdminToken: string;
-    let regularUserToken: string;
-
-    beforeEach(async () => {
-      adminClient = testClient(unifiedFilesAdminRoutes);
-
-      // 生成超级管理员token (ID=1)
-      superAdminToken = JWTUtil.generateToken({
-        id: 1,
-        username: 'admin',
-        roles: [{ name: 'admin' }]
-      });
-
-      // 生成普通用户token (ID=2)
-      regularUserToken = JWTUtil.generateToken({
-        id: 2,
-        username: 'user',
-        roles: [{ name: 'user' }]
-      });
-    });
-
-    describe('GET /admin/unified-files', () => {
-      it('应该允许超级管理员获取文件列表', async () => {
-        const response = await adminClient.index.$get({
-          query: { page: 1, pageSize: 10 }
-        }, {
-          headers: {
-            'Authorization': `Bearer ${superAdminToken}`
-          }
-        });
-
-        console.debug('管理员文件列表响应状态:', response.status);
-        expect(response.status).toBe(200);
-
-        if (response.status === 200) {
-          const data = await response.json();
-          expect(data).toHaveProperty('code', 200);
-          expect(data).toHaveProperty('data');
-          expect(data.data).toHaveProperty('list');
-          expect(Array.isArray(data.data.list)).toBe(true);
-        }
-      });
-
-      it('应该拒绝普通用户访问管理员接口', async () => {
-        const response = await adminClient.index.$get({
-          query: { page: 1, pageSize: 10 }
-        }, {
-          headers: {
-            'Authorization': `Bearer ${regularUserToken}`
-          }
-        });
-
-        expect(response.status).toBe(403);
-      });
-
-      it('应该拒绝未认证用户访问', async () => {
-        const response = await adminClient.index.$get({
-          query: { page: 1, pageSize: 10 }
-        });
-
-        expect(response.status).toBe(401);
-      });
-    });
-
-    describe('POST /admin/unified-files', () => {
-      it('应该允许超级管理员创建文件记录', async () => {
-        const newFile = {
-          fileName: 'test-banner.jpg',
-          filePath: 'unified/test-banner.jpg',
-          fileSize: 102400,
-          mimeType: 'image/jpeg',
-          status: 1
-        };
-
-        const response = await adminClient.index.$post({
-          json: newFile
-        }, {
-          headers: {
-            'Authorization': `Bearer ${superAdminToken}`
-          }
-        });
-
-        console.debug('创建文件响应状态:', response.status);
-        expect([200, 201]).toContain(response.status);
-      });
-
-      it('应该拒绝普通用户创建文件', async () => {
-        const newFile = {
-          fileName: 'test-banner.jpg',
-          filePath: 'unified/test-banner.jpg',
-          fileSize: 102400,
-          mimeType: 'image/jpeg'
-        };
-
-        const response = await adminClient.index.$post({
-          json: newFile
-        }, {
-          headers: {
-            'Authorization': `Bearer ${regularUserToken}`
-          }
-        });
-
-        expect(response.status).toBe(403);
-      });
-    });
-
-    describe('DELETE /admin/unified-files/:id', () => {
-      it('应该允许超级管理员软删除文件', async () => {
-        const dataSource = await IntegrationTestDatabase.getDataSource();
-        const fileRepository = dataSource.getRepository(UnifiedFile);
-
-        const testFile = fileRepository.create({
-          fileName: 'to-delete.jpg',
-          filePath: 'unified/to-delete.jpg',
-          fileSize: 51200,
-          mimeType: 'image/jpeg',
-          status: 1
-        });
-        await fileRepository.save(testFile);
-
-        const response = await adminClient[':id'].$delete({
-          param: { id: testFile.id }
-        }, {
-          headers: {
-            'Authorization': `Bearer ${superAdminToken}`
-          }
-        });
-
-        console.debug('删除文件响应状态:', response.status);
-        expect(response.status).toBe(200);
-
-        // 验证软删除
-        const deletedFile = await fileRepository.findOne({
-          where: { id: testFile.id }
-        });
-        expect(deletedFile).toBeDefined();
-        expect(deletedFile?.status).toBe(0);
-      });
-    });
-  });
-});

+ 0 - 75
packages/unified-file-module/tests/unit/unified-file.service.unit.test.ts

@@ -1,75 +0,0 @@
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import { DataSource } from 'typeorm';
-import * as ServiceModule from '../../src/services/unified-file.service';
-import { UnifiedFile } from '../../src/entities/unified-file.entity';
-
-const { UnifiedFileService } = ServiceModule;
-
-describe('UnifiedFileService', () => {
-  let service: InstanceType<typeof UnifiedFileService>;
-  let mockDataSource: Partial<DataSource>;
-
-  beforeEach(() => {
-    mockDataSource = {
-      getRepository: vi.fn()
-    };
-    service = new UnifiedFileService(mockDataSource as DataSource);
-  });
-
-  describe('create', () => {
-    it('should create file record with status=1', async () => {
-      const fileData: Partial<UnifiedFile> = {
-        fileName: 'test.jpg',
-        filePath: 'unified/test.jpg',
-        createdBy: 1
-      };
-
-      const mockEntity = { ...fileData, id: 1, status: 1, createdAt: new Date(), updatedAt: new Date() };
-
-      const repository = {
-        create: vi.fn().mockReturnValue(mockEntity),
-        save: vi.fn().mockResolvedValue(mockEntity),
-        update: vi.fn().mockResolvedValue(undefined),
-        findOne: vi.fn().mockResolvedValue(mockEntity)
-      };
-      (mockDataSource.getRepository as any).mockReturnValue(repository);
-
-      // 设置 repository 到 service 实例
-      service['repository'] = repository as any;
-
-      const result = await service.create(fileData, 1);
-
-      expect(repository.create).toHaveBeenCalled();
-      expect(result.status).toBe(1);
-    });
-  });
-
-  describe('delete (soft delete)', () => {
-    it('should set status to 0 instead of physical delete', async () => {
-      const fileData: Partial<UnifiedFile> = {
-        id: 1,
-        fileName: 'test.jpg',
-        filePath: 'unified/test.jpg',
-        status: 1,
-        createdBy: 1
-      };
-
-      const updatedData = { ...fileData, status: 0, updatedAt: new Date() };
-
-      const repository = {
-        create: vi.fn(),
-        save: vi.fn().mockResolvedValue(updatedData),
-        update: vi.fn().mockResolvedValue({ affected: 1 }),
-        findOne: vi.fn().mockResolvedValue(fileData)
-      };
-      (mockDataSource.getRepository as any).mockReturnValue(repository);
-
-      service['repository'] = repository as any;
-
-      const result = await service.delete(1, 1);
-
-      expect(result).toBe(true);
-      expect(repository.update).toHaveBeenCalledWith(1, expect.objectContaining({ status: 0 }));
-    });
-  });
-});

+ 0 - 34
packages/unified-file-module/tests/utils/integration-test-db.ts

@@ -1,34 +0,0 @@
-import { DataSource } from "typeorm";
-import { UnifiedFile } from "../../src/entities";
-
-/**
- * 测试数据工厂类
- */
-export class TestDataFactory {
-  /**
-   * 创建测试文件数据
-   */
-  static createFileData(overrides: Partial<UnifiedFile> = {}): Partial<UnifiedFile> {
-    const timestamp = Date.now();
-    return {
-      fileName: `testfile_${timestamp}.txt`,
-      mimeType: "text/plain",
-      fileSize: 1024,
-      filePath: `/uploads/testfile_${timestamp}.txt`,
-      description: `Test file ${timestamp}`,
-      createdAt: new Date(),
-      ...overrides
-    };
-  }
-
-  /**
-   * 在数据库中创建测试文件
-   */
-  static async createTestFile(dataSource: DataSource, overrides: Partial<UnifiedFile> = {}): Promise<UnifiedFile> {
-    const fileData = this.createFileData(overrides);
-    const fileRepository = dataSource.getRepository(UnifiedFile);
-
-    const file = fileRepository.create(fileData);
-    return await fileRepository.save(file);
-  }
-}

+ 0 - 106
packages/unified-file-module/tests/utils/integration-test-utils.ts

@@ -1,106 +0,0 @@
-import { IntegrationTestDatabase } from "@d8d/shared-test-util";
-import { UnifiedFile } from "../../src/entities";
-
-/**
- * 集成测试断言工具
- */
-export class IntegrationTestAssertions {
-  /**
-   * 断言响应状态码
-   */
-  static expectStatus(response: { status: number }, expectedStatus: number): void {
-    if (response.status !== expectedStatus) {
-      throw new Error(`Expected status ${expectedStatus}, but got ${response.status}`);
-    }
-  }
-
-  /**
-   * 断言响应包含特定字段
-   */
-  static expectResponseToHave(response: { data: any }, expectedFields: Record<string, any>): void {
-    for (const [key, value] of Object.entries(expectedFields)) {
-      if (response.data[key] !== value) {
-        throw new Error(`Expected field ${key} to be ${value}, but got ${response.data[key]}`);
-      }
-    }
-  }
-
-  /**
-   * 断言响应包含特定结构
-   */
-  static expectResponseStructure(response: { data: any }, structure: Record<string, any>): void {
-    for (const key of Object.keys(structure)) {
-      if (!(key in response.data)) {
-        throw new Error(`Expected response to have key: ${key}`);
-      }
-    }
-  }
-
-  /**
-   * 断言文件存在于数据库中
-   */
-  static async expectFileToExist(fileName: string): Promise<void> {
-    const dataSource = await IntegrationTestDatabase.getDataSource();
-    if (!dataSource) {
-      throw new Error("Database not initialized");
-    }
-
-    const fileRepository = dataSource.getRepository(UnifiedFile);
-    const file = await fileRepository.findOne({ where: { fileName } });
-
-    if (!file) {
-      throw new Error(`Expected file ${fileName} to exist in database`);
-    }
-  }
-
-  /**
-   * 断言文件不存在于数据库中
-   */
-  static async expectFileNotToExist(fileName: string): Promise<void> {
-    const dataSource = await IntegrationTestDatabase.getDataSource();
-    if (!dataSource) {
-      throw new Error("Database not initialized");
-    }
-
-    const fileRepository = dataSource.getRepository(UnifiedFile);
-    const file = await fileRepository.findOne({ where: { fileName } });
-
-    if (file) {
-      throw new Error(`Expected file ${fileName} not to exist in database`);
-    }
-  }
-
-  /**
-   * 断言文件存在于数据库中(通过ID)
-   */
-  static async expectFileToExistById(id: number): Promise<void> {
-    const dataSource = await IntegrationTestDatabase.getDataSource();
-    if (!dataSource) {
-      throw new Error("Database not initialized");
-    }
-
-    const fileRepository = dataSource.getRepository(UnifiedFile);
-    const file = await fileRepository.findOne({ where: { id } });
-
-    if (!file) {
-      throw new Error(`Expected file with ID ${id} to exist in database`);
-    }
-  }
-
-  /**
-   * 断言文件不存在于数据库中(通过ID)
-   */
-  static async expectFileNotToExistById(id: number): Promise<void> {
-    const dataSource = await IntegrationTestDatabase.getDataSource();
-    if (!dataSource) {
-      throw new Error("Database not initialized");
-    }
-
-    const fileRepository = dataSource.getRepository(UnifiedFile);
-    const file = await fileRepository.findOne({ where: { id } });
-
-    if (file) {
-      throw new Error(`Expected file with ID ${id} not to exist in database`);
-    }
-  }
-}

+ 0 - 16
packages/unified-file-module/tsconfig.json

@@ -1,16 +0,0 @@
-{
-  "extends": "../../tsconfig.json",
-  "compilerOptions": {
-    "composite": true,
-    "rootDir": ".",
-    "outDir": "dist"
-  },
-  "include": [
-    "src/**/*",
-    "tests/**/*"
-  ],
-  "exclude": [
-    "node_modules",
-    "dist"
-  ]
-}

+ 0 - 21
packages/unified-file-module/vitest.config.ts

@@ -1,21 +0,0 @@
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
-  test: {
-    globals: true,
-    environment: 'node',
-    include: ['tests/**/*.test.ts'],
-    coverage: {
-      provider: 'v8',
-      reporter: ['text', 'json', 'html'],
-      exclude: [
-        'node_modules/',
-        'dist/',
-        'tests/',
-        '**/*.d.ts'
-      ]
-    },
-    // 关闭并行测试以避免数据库连接冲突
-    fileParallelism: false
-  }
-});