2
0
Эх сурвалжийг харах

feat: 创建故事 010.009 - 创建统一文件后端模块

- 从单租户文件模块复制并改造创建统一文件后端模块
- 使用 cp -r 命令直接复制整个文件夹
- 移除用户模块和认证模块依赖
- 所有路由使用 tenantAuthMiddleware
- 删除 UserEntity 关联,只保留 uploadUserId 字段

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 долоо хоног өмнө
parent
commit
fab85fadcd

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

@@ -0,0 +1,389 @@
+# Story 010.009: 创建统一文件后端模块 (unified-file-module)
+
+## Status
+Approved
+
+## Story
+
+**As a** 开发者,
+**I want** 从单租户文件模块复制并改造创建统一文件后端模块(unified-file-module),
+**so that** 统一广告模块可以使用无租户隔离的文件实体,确保架构一致性。
+
+## Acceptance Criteria
+
+1. 创建 `packages/unified-file-module` 包(**使用 `cp -r` 命令直接复制 `packages/file-module` 整个文件夹**)
+2. 修改包名和模块引用(从 `@d8d/file-module` 改为 `@d8d/unified-file-module`),**移除用户模块和认证模块依赖**
+3. 定义Entity(确认无tenant_id字段,单租户file-module本身就没有tenant_id)
+4. 实现Service层和文件上传逻辑(MinIO,保持与原模块一致)
+5. 实现管理员路由(**所有路由**都使用 `tenantAuthMiddleware`,只有超级管理员可访问,**不需要用户展示路由**)
+6. 编写完整的单元测试和集成测试
+7. 测试覆盖率达到70%以上
+
+## Tasks / Subtasks
+
+- [ ] **任务1: 复制文件模块创建统一文件模块** (AC: 1)
+  - [ ] **使用 `cp -r packages/file-module packages/unified-file-module` 命令直接复制整个文件夹**
+  - [ ] 验证复制后的目录结构完整
+  - [ ] 验证所有文件都已复制
+
+- [ ] **任务2: 修改包配置文件** (AC: 2)
+  - [ ] 修改 `package.json` 包名:`@d8d/file-module` → `@d8d/unified-file-module`
+  - [ ] 修改 `package.json` 描述:添加"unified"相关描述
+  - [ ] **删除依赖**:从 `package.json` 中移除 `@d8d/user-module` 和 `@d8d/auth-module`(本模块只在超级管理员后台使用,不需要用户和认证依赖)
+  - [ ] **添加依赖**:添加 `@d8d/tenant-module-mt`(需要使用 `tenantAuthMiddleware`)
+
+- [ ] **任务3: 修改模块导出文件** (AC: 2)
+  - [ ] 修改 `src/index.ts` 导出的实体名:`File` → `UnifiedFile`
+  - [ ] 修改 `src/index.ts` 导出的服务名:`FileService` → `UnifiedFileService`,`MinioService` 保持不变
+  - [ ] 更新 `src/entities/index.ts` 导出
+
+- [ ] **任务4: 修改Entity定义** (AC: 3)
+  - [ ] 重命名文件:`entities/file.entity.ts` → `entities/unified-file.entity.ts`
+  - [ ] 修改类名:`export class File` → `export class UnifiedFile`
+  - [ ] 确认无 `tenant_id` 字段(单租户模块本身就没有,验证确认即可)
+  - [ ] 修改表名:`@Entity('file')` → `@Entity('unified_file')`
+  - [ ] **删除 UserEntity 关联**:移除 `@ManyToOne('UserEntity')` 和 `uploadUser` 字段(因为不需要用户模块依赖)
+  - [ ] 保持 `uploadUserId` 字段不变(只保留ID,不关联UserEntity)
+  - [ ] 保持其他所有字段定义不变(id, name, type, size, path, description, uploadTime, lastUpdated, createdAt, updatedAt)
+
+- [ ] **任务5: 修改Service层** (AC: 4)
+  - [ ] 重命名文件:`services/file.service.ts` → `services/unified-file.service.ts`
+  - [ ] 修改类名:`export class FileService` → `export class UnifiedFileService`
+  - [ ] 更新构造函数中的Entity引用:`File` → `UnifiedFile`
+  - [ ] `MinioService` 保持不变(复用原有实现)
+  - [ ] 保持所有方法逻辑不变
+
+- [ ] **任务6: 修改Schema定义** (AC: 4)
+  - [ ] 修改 `schemas/file.schema.ts` 中所有Schema名称:`FileSchema` → `UnifiedFileSchema`,`CreateFileSchema` → `CreateUnifiedFileSchema`,等等
+  - [ ] 更新Schema中的描述,添加"unified"相关说明
+  - [ ] 保持所有字段验证规则不变
+
+- [ ] **任务7: 修改路由层** (AC: 5)
+  - [ ] **所有路由都使用 `tenantAuthMiddleware`**(只有超级管理员ID=1可访问,**不需要用户展示路由**)
+  - [ ] 删除原有的用户展示路由(如果存在)
+  - [ ] 确认路由路径设置为相对路径(不包含 `/api/v1` 前缀)
+  - [ ] 添加 `request.params` 定义,使用 `z.coerce.number<number>()` 进行类型转换
+
+- [ ] **任务8: 编写单元测试** (AC: 7, 8)
+  - [ ] 重命名测试文件:`tests/unit/file.service.test.ts` → `tests/unit/unified-file.service.test.ts`
+  - [ ] 更新测试中的实体名:`File` → `UnifiedFile`
+  - [ ] 更新测试中的服务名:`FileService` → `UnifiedFileService`
+  - [ ] 验证所有单元测试通过
+
+- [ ] **任务9: 编写集成测试** (AC: 6, 7)
+  - [ ] 重命名测试文件:`tests/integration/file.integration.test.ts` → `tests/integration/unified-file.integration.test.ts`
+  - [ ] 更新测试中的实体名和服务名
+  - [ ] 添加管理员路由权限测试(验证只有超级管理员ID=1可访问)
+  - [ ] **删除用户路由测试**(不需要用户展示路由)
+  - [ ] 验证所有集成测试通过
+
+- [ ] **任务10: 类型检查和代码质量** (AC: 8)
+  - [ ] 运行 `pnpm typecheck` 确保无TypeScript类型错误
+  - [ ] 运行 `pnpm test` 确保所有测试通过
+  - [ ] 运行 `pnpm test:coverage` 确保测试覆盖率达到70%以上
+
+## Dev Notes
+
+### 前一故事关键要点(来自 010.006, 010.007, 010.008)
+
+**史诗010的统一设计模式**:
+- **⚠️ 统一文件模块只在超级管理员后台使用,不需要用户展示路由**
+- **所有路由都使用 `tenantAuthMiddleware`**(只有超级管理员ID=1可访问)
+- **不需要 `authMiddleware`**(不需要用户展示路由)
+- **不需要依赖用户模块和认证模块**
+- 统一模块的Entity没有 `tenant_id` 字段
+- API路由路径使用相对路径(不包含 `/api/v1` 前缀)
+
+**路由规范** [Source: docs/prd/epic-010-unified-ad-management.md, docs/architecture/backend-module-package-standards.md]:
+- 路由路径必须使用相对路径(如 `/` 和 `/:id`),不包含 `/api/v1` 前缀
+- 路由 `request.params` 必须明确定义,使用 `z.coerce.number<number>()` 进行类型转换
+- 使用 `c.req.valid('param')` 获取参数,而非 `parseInt(c.req.param('id'))`
+
+### 当前问题说明
+
+**架构不一致性** [Source: docs/prd/epic-010-unified-ad-management.md]:
+- 统一广告模块 (`unified-advertisements-module`) 当前使用 `@d8d/core-module-mt/file-module-mt` 的 `FileMt` 实体
+- `FileMt` 实体有 `tenant_id` 字段,是多租户隔离的
+- 统一广告本身是无租户隔离的,但关联的文件却是多租户隔离的
+- 这造成了架构不一致性
+
+### 源模块:单租户文件模块 (file-module)
+
+**模块结构** [Source: packages/file-module/]:
+```
+packages/file-module/
+├── package.json
+├── tsconfig.json
+├── vitest.config.ts
+├── src/
+│   ├── entities/
+│   │   └── file.entity.ts
+│   ├── services/
+│   │   ├── file.service.ts
+│   │   └── minio.service.ts
+│   ├── routes/
+│   │   ├── file.routes.ts
+│   │   ├── file-crud.routes.ts
+│   │   ├── file-custom.routes.ts
+│   │   └── index.ts
+│   ├── schemas/
+│   │   └── file.schema.ts
+│   └── index.ts
+└── tests/
+    ├── unit/
+    │   └── file.service.test.ts
+    └── integration/
+        └── file.integration.test.ts
+```
+
+**Entity定义** [Source: packages/file-module/src/entities/file.entity.ts]:
+```typescript
+@Entity('file')
+export class File {
+  @PrimaryGeneratedColumn({ name: 'id', type: 'int', unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255 })
+  name!: string;
+
+  @Column({ name: 'type', type: 'varchar', length: 50, nullable: true, comment: '文件类型' })
+  type!: string | null;
+
+  @Column({ name: 'size', type: 'int', unsigned: true, nullable: true, comment: '文件大小,单位字节' })
+  size!: number | null;
+
+  @Column({ name: 'path', type: 'varchar', length: 512, comment: '文件存储路径' })
+  path!: string;
+
+  @Column({ name: 'description', type: 'text', nullable: true, comment: '文件描述' })
+  description!: string | null;
+
+  @Column({ name: 'upload_user_id', type: 'int', unsigned: true })
+  uploadUserId!: number;
+
+  // ⚠️ 统一文件模块删除此关联(不需要用户模块依赖)
+  // @ManyToOne('UserEntity')
+  // @JoinColumn({ name: 'upload_user_id', referencedColumnName: 'id' })
+  // uploadUser!: UserEntity;
+
+  @Column({ name: 'upload_time', type: 'timestamp' })
+  uploadTime!: Date;
+
+  @Column({ name: 'last_updated', type: 'timestamp', nullable: true, comment: '最后更新时间' })
+  lastUpdated!: Date | null;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
+  createdAt!: Date;
+
+  @Column({
+    name: 'updated_at',
+    type: 'timestamp',
+    default: () => 'CURRENT_TIMESTAMP',
+    onUpdate: 'CURRENT_TIMESTAMP'
+  })
+  updatedAt!: Date;
+
+  // 注意:没有 tenant_id 字段(单租户模块)
+}
+```
+
+**包名**: `@d8d/file-module` [Source: packages/file-module/package.json]
+
+**依赖** [Source: packages/file-module/package.json]:
+```json
+{
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/tenant-module-mt": "workspace:*",
+    "hono": "^4.8.5",
+    "@hono/zod-openapi": "1.0.2",
+    "minio": "^8.0.5",
+    "typeorm": "^0.3.20",
+    "uuid": "^11.1.0",
+    "zod": "^4.1.12"
+  }
+}
+```
+
+**⚠️ 依赖变更说明**:
+- **删除**: `@d8d/user-module` 和 `@d8d/auth-module`(本模块只在超级管理员后台使用,不需要这些依赖)
+- **添加**: `@d8d/tenant-module-mt`(需要使用 `tenantAuthMiddleware`)
+
+### 关键实施要点
+
+**⚠️ 重要:使用 CP 命令直接复制**
+
+根据用户明确要求,必须使用以下命令直接复制整个文件模块文件夹:
+
+```bash
+cp -r packages/file-module packages/unified-file-module
+```
+
+**为什么使用 cp 命令直接复制**:
+1. **保证完整性**: 复制整个文件夹确保所有文件(包括测试、配置、源码)都被包含
+2. **减少错误**: 避免手动创建文件时遗漏某些文件或配置
+3. **保持一致性**: 确保新模块的结构与原模块完全一致
+4. **提高效率**: 一次性复制后只需修改必要的部分,而不是从头创建
+
+**复制后的修改清单**:
+
+1. **包名修改**: `@d8d/file-module` → `@d8d/unified-file-module`
+2. **依赖修改**: 删除 `@d8d/user-module` 和 `@d8d/auth-module`,添加 `@d8d/tenant-module-mt`
+3. **实体名修改**: `File` → `UnifiedFile`,表名 `file` → `unified_file`
+4. **实体关联修改**: 删除 `UserEntity` 关联,只保留 `uploadUserId` 字段
+5. **服务名修改**: `FileService` → `UnifiedFileService`
+6. **Schema名修改**: 所有Schema添加 `Unified` 前缀
+7. **文件名修改**: 测试文件和服务文件重命名
+8. **路由配置**: **所有路由**都使用 `tenantAuthMiddleware`,**不需要用户展示路由**
+
+### 后端模块包开发规范
+
+**包结构规范** [Source: docs/architecture/backend-module-package-standards.md]:
+```
+packages/{module-name}/
+├── package.json
+├── tsconfig.json
+├── vitest.config.ts
+├── src/
+│   ├── entities/
+│   ├── services/
+│   ├── routes/
+│   ├── schemas/
+│   └── index.ts
+└── tests/
+    ├── integration/
+    └── utils/
+```
+
+**Entity规范** [Source: docs/architecture/backend-module-package-standards.md]:
+- 使用 `@Entity()` 装饰器定义表名
+- 完整的列定义:包含 `type`, `length`, `nullable`, `comment` 等属性
+- 主键使用 `@PrimaryGeneratedColumn`,设置 `unsigned: true`
+- 时间戳使用 `timestamp` 类型,默认值为 `() => 'CURRENT_TIMESTAMP'`
+
+**Service规范** [Source: docs/architecture/backend-module-package-standards.md]:
+- 继承 `GenericCrudService` 基类
+- 使用 `override` 关键字覆盖父类方法
+- 软删除使用 `status` 字段(如果需要)
+
+**路由规范** [Source: docs/architecture/backend-module-package-standards.md]:
+- 使用 `OpenAPIHono` 和 `AuthContext` 泛型
+- 路由路径使用相对路径,不包含 `/api/v1` 前缀
+- 自定义路由使用 `createRoute` 定义
+- 响应数据使用 `parseWithAwait` 验证
+- `request.params` 使用 `z.coerce.number<number>()` 进行类型转换
+
+**Schema规范** [Source: docs/architecture/backend-module-package-standards.md]:
+- 使用 `z.object()` 定义Schema
+- 使用 `.openapi()` 装饰器添加描述和示例
+- 使用 `z.coerce.date<Date>()` 和 `z.coerce.number<number>()` 泛型语法
+- 不导出推断类型(类型由RPC自动推断)
+
+### 核心模块引用规范
+
+**⚠️ 重要**: 新模块必须从正确路径引用基础模块
+
+| 模块类型 | ✅ 正确引用 | 说明 |
+|---------|-----------|------|
+| 租户模块(多租户) | `@d8d/tenant-module-mt` | 独立包,位于 `packages/tenant-module-mt/` |
+
+**⚠️ 本模块不需要的依赖**:
+- ❌ 不需要 `@d8d/user-module` 或 `@d8d/user-module-mt`(不在用户模块中关联用户信息)
+- ❌ 不需要 `@d8d/auth-module` 或 `@d8d/auth-module-mt`(不需要用户认证中间件)
+- ✅ 只需要 `@d8d/tenant-module-mt`(使用 `tenantAuthMiddleware`)
+
+### MinIO集成
+
+**MinIO配置** [Source: docs/architecture/tech-stack.md]:
+- 对象存储服务:MinIO
+- 客户端SDK:MinIO JavaScript SDK
+- 默认存储桶:`d8dai`
+
+**环境变量**:
+- `MINIO_HOST`: MinIO服务器地址
+- `MINIO_PORT`: MinIO端口
+- `MINIO_USE_SSL`: 是否使用SSL
+- `MINIO_BUCKET_NAME`: 存储桶名称
+
+### 项目位置
+
+**新模块位置**: `packages/unified-file-module/` [Source: docs/architecture/source-tree.md]
+
+**测试标准**: Vitest [Source: docs/architecture/tech-stack.md]
+
+### Testing
+
+**测试文件位置** [Source: docs/architecture/backend-module-package-standards.md]:
+- 单元测试: `packages/unified-file-module/tests/unit/`
+- 集成测试: `packages/unified-file-module/tests/integration/`
+
+**测试框架**:
+- Vitest [Source: docs/architecture/tech-stack.md]
+- hono/testing (API端点测试)
+
+**测试配置** [Source: docs/architecture/backend-module-package-standards.md]:
+```typescript
+// vitest.config.ts
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    fileParallelism: false // 避免数据库连接冲突
+  }
+});
+```
+
+**测试覆盖要求**:
+- 测试覆盖率达到70%以上
+- 单元测试:覆盖Service层核心逻辑
+- 集成测试:覆盖路由端点和权限控制
+
+**测试命令**:
+```bash
+# 进入模块目录
+cd packages/unified-file-module
+
+# 运行所有测试
+pnpm test
+
+# 运行单元测试
+pnpm test:unit
+
+# 运行集成测试
+pnpm test:integration
+
+# 生成覆盖率报告
+pnpm test:coverage
+
+# 类型检查
+pnpm typecheck
+```
+
+## Change Log
+
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2026-01-04 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+| 2026-01-04 | 1.1 | 修正:统一文件模块只在超级管理员后台使用,移除用户模块和认证模块依赖,所有路由使用 tenantAuthMiddleware | Bob (Scrum Master) |
+| 2026-01-04 | 1.2 | 修正:租户模块是独立包 `@d8d/tenant-module-mt`,不在 `core-module-mt` 里面 | Bob (Scrum Master) |
+| 2026-01-04 | 1.3 | 批准故事 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+### Agent Model Used
+_开发代理实施时填写_
+
+### Debug Log References
+_开发代理实施时填写_
+
+### Completion Notes List
+_开发代理实施时填写_
+
+### File List
+_开发代理实施时填写_
+
+## QA Results
+_QA代理待填写_