Browse Source

docs: 添加故事 010.011 - 集成统一文件模块到统一广告和租户后台

创建故事 010.011,将统一文件模块集成到:
- 统一广告模块(更新 Entity 从 FileMt 到 UnifiedFile)
- 统一广告管理UI(更新文件选择器)
- Server包(注册路由和实体)
- 租户后台(添加菜单和路由)

验收标准:
1. 统一广告模块使用 UnifiedFile 实体
2. 统一广告管理UI使用统一文件选择器
3. Server包注册统一文件模块
4. 租户后台集成文件管理功能
5. E2E测试验证
6. 回归测试确保功能不受影响

🤖 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 tuần trước cách đây
mục cha
commit
27071a1509
1 tập tin đã thay đổi với 337 bổ sung0 xóa
  1. 337 0
      docs/stories/010.011.story.md

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

@@ -0,0 +1,337 @@
+# Story 010.011: 集成统一文件模块到统一广告和租户后台
+
+## Status
+Approved
+
+## Story
+
+**As a** 开发者,
+**I want** 将统一文件模块集成到统一广告模块、统一广告管理UI、Server包和租户后台,
+**so that** 统一广告系统使用无租户隔离的文件管理,确保架构一致性。
+
+## Acceptance Criteria
+
+1. 统一广告模块更新为使用 `UnifiedFile` 实体(而非 `FileMt`)
+2. 统一广告管理UI更新为使用统一文件选择器(而非多租户版本)
+3. Server包注册统一文件模块路由和实体
+4. 租户后台集成统一文件管理功能(菜单项、路由)
+5. E2E测试验证文件上传和选择器功能
+6. 回归测试确保统一广告模块功能不受影响
+
+## Tasks / Subtasks
+
+- [ ] **任务1: 更新统一广告模块使用 UnifiedFile 实体** (AC: 1)
+  - [ ] 修改 `UnifiedAdvertisement` Entity 的 `imageFile` 关联:`FileMt` → `UnifiedFile`
+  - [ ] 更新导入路径:`@d8d/core-module-mt/file-module-mt` → `@d8d/unified-file-module`
+  - [ ] 验证关联查询字段名一致(`imageFileId`, `imageFile`)
+
+- [ ] **任务2: 更新统一广告管理UI使用统一文件选择器** (AC: 2)
+  - [ ] 修改 `package.json` 依赖:移除 `@d8d/file-management-ui-mt`,添加 `@d8d/unified-file-management-ui`
+  - [ ] 修改组件导入:`FileSelector` from `@d8d/file-management-ui-mt` → `@d8d/unified-file-management-ui`
+  - [ ] 验证API客户端初始化指向正确端点(`/api/v1/admin/unified-files`)
+  - [ ] 更新测试中的mock路由和API
+
+- [ ] **任务3: Server包注册统一文件模块** (AC: 3)
+  - [ ] 在 `packages/server/src/index.ts` 添加导入:`import { UnifiedFile } from '@d8d/unified-file-module'`
+  - [ ] 在 `initializeDataSource` 添加实体:`UnifiedFile`
+  - [ ] 在 `packages/server/src/index.ts` 添加路由导入:`import { unifiedFileRoutes } from '@d8d/unified-file-module'`
+  - [ ] 注册管理员路由:`export const adminUnifiedFileApiRoutes = api.route('/api/v1/admin/unified-files', unifiedFileRoutes)`
+
+- [ ] **任务4: 租户后台集成统一文件管理功能** (AC: 4)
+  - [ ] 在 `web/src/client/tenant/routes.tsx` 添加路由:
+    - 导入 `FileManagement` from `@d8d/unified-file-management-ui`
+    - 添加路径:`/tenant/files` → `<FileManagement />`
+  - [ ] 在 `web/src/client/tenant/menu.tsx` 添加菜单项:
+    - 添加文件管理菜单(File图标,路径 `/tenant/files`)
+  - [ ] 在 `web/src/client/tenant/api_init.ts` 初始化统一文件API客户端
+
+- [ ] **任务5: E2E测试验证文件上传和选择器功能** (AC: 5)
+  - [ ] 创建E2E测试:`web/tests/e2e/unified-file-management.spec.ts`
+  - [ ] 测试文件上传功能(MinIO集成)
+  - [ ] 测试文件选择器在广告创建/编辑中的集成
+  - [ ] 测试文件删除功能
+
+- [ ] **任务6: 回归测试确保统一广告模块功能不受影响** (AC: 6)
+  - [ ] 运行统一广告模块集成测试:`cd packages/unified-advertisements-module && pnpm test`
+  - [ ] 运行统一广告管理UI测试:`cd packages/unified-advertisement-management-ui && pnpm test`
+  - [ ] 运行Server包集成测试验证广告API功能正常
+  - [ ] 验证现有广告数据可正常访问图片
+
+- [ ] **任务7: 类型检查和代码质量** (AC: 1-6)
+  - [ ] 运行 `pnpm typecheck` 确保无TypeScript类型错误
+  - [ ] 运行 `pnpm lint` 确保代码规范检查通过
+  - [ ] 运行 `pnpm test` 确保所有测试通过
+
+## Dev Notes
+
+### 前一故事关键要点
+
+**来自故事 010.009(统一文件后端模块)**:
+- 统一文件模块路由使用 `tenantAuthMiddleware`(超级管理员专用)
+- API路由路径:`/api/v1/admin/unified-files`(管理员路由)
+- Entity: `UnifiedFile`,无 `tenant_id` 字段
+- 数据库表名:`unified_file`
+- 字段:`id`, `name`, `type`, `size`, `path`, `description`, `uploadUserId`, `uploadTime`, `lastUpdated`, `createdAt`, `updatedAt`
+- 测试覆盖:22个测试全部通过(14个单元测试 + 8个集成测试)
+
+**来自故事 010.010(统一文件管理UI包)**:
+- 统一文件管理UI提供 `FileManagement` 和 `FileSelector` 组件
+- API客户端管理器:`UnifiedFileClientManager`
+- 测试覆盖:30个测试全部通过(9个hook测试 + 21个组件测试)
+- 使用RPC推断类型
+
+### 当前统一广告模块的文件关联
+
+**统一广告模块 Entity** [Source: packages/unified-advertisements-module/src/entities/unified-advertisement.entity.ts]:
+```typescript
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
+
+@Entity('ad_unified')
+export class UnifiedAdvertisement {
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => FileMt, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: FileMt | null;
+}
+```
+
+**需要修改**:
+- 导入路径:`@d8d/core-module-mt/file-module-mt` → `@d8d/unified-file-module`
+- 类型定义:`FileMt` → `UnifiedFile`
+
+### 统一文件模块结构
+
+**包位置**: `packages/unified-file-module/` [Source: docs/architecture/source-tree.md]
+
+**导出** [Source: packages/unified-file-module/src/index.ts]:
+```typescript
+// 实体
+export { UnifiedFile } from './entities';
+
+// 服务
+export { UnifiedFileService, MinioService } from './services';
+
+// Schema
+export * from './schemas';
+
+// 路由
+export { default as unifiedFileRoutes } from './routes';
+```
+
+**Entity定义** [Source: packages/unified-file-module/src/entities/unified-file.entity.ts]:
+```typescript
+@Entity('unified_file')
+export class UnifiedFile {
+  @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;
+
+  @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;
+}
+```
+
+**路由结构** [Source: packages/unified-file-module/src/routes/]:
+- 所有路由使用 `tenantAuthMiddleware`(超级管理员专用)
+- 路由路径:`/` (列表/创建), `/[:id]` (详情/更新/删除)
+- multipart-complete 路由(完整文件上传)
+- multipart-policy 路由(分片上传策略)
+- upload-policy 路由(上传策略)
+
+### 统一文件管理UI结构
+
+**包位置**: `packages/unified-file-management-ui/` [Source: docs/architecture/source-tree.md]
+
+**组件导出** [Source: packages/unified-file-management-ui/src/components/index.ts]:
+```typescript
+export { FileManagement } from './FileManagement';
+export { default as FileSelector } from './FileSelector';
+export { default as MinioUploader } from './MinioUploader';
+```
+
+**API客户端** [Source: packages/unified-file-management-ui/src/api/]:
+- `unifiedFileClient.ts`: RPC客户端管理器
+- `UnifiedFileClientManager`: 单例模式管理客户端
+- API端点:`/api/v1/admin/unified-files`
+
+### Server包注册模式
+
+**当前统一广告模块注册** [Source: packages/server/src/index.ts]:
+```typescript
+// 导入实体
+import { UnifiedAdvertisement, UnifiedAdvertisementType } from '@d8d/unified-advertisements-module'
+
+// 注册实体
+initializeDataSource([
+  UnifiedAdvertisement, UnifiedAdvertisementType,
+  // ...
+])
+
+// 导入和注册路由
+import {
+  unifiedAdvertisementRoutes,
+  unifiedAdvertisementAdminRoutes,
+} from '@d8d/unified-advertisements-module'
+
+export const unifiedAdvertisementApiRoutes = api.route('/api/v1', unifiedAdvertisementRoutes)
+export const adminUnifiedAdvertisementApiRoutes = api.route('/api/v1/admin/unified-advertisements', unifiedAdvertisementAdminRoutes)
+```
+
+**需要添加统一文件模块注册**:
+```typescript
+// 导入实体
+import { UnifiedFile } from '@d8d/unified-file-module'
+
+// 注册实体到 initializeDataSource
+initializeDataSource([
+  // ...
+  UnifiedFile,
+])
+
+// 导入和注册路由
+import { unifiedFileRoutes } from '@d8d/unified-file-module'
+
+// 注册管理员路由(统一文件模块只有管理员路由)
+export const adminUnifiedFileApiRoutes = api.route('/api/v1/admin/unified-files', unifiedFileRoutes)
+```
+
+### 租户后台集成模式
+
+**路由配置** [Source: web/src/client/tenant/routes.tsx]:
+```typescript
+import { UnifiedAdvertisementManagement } from '@d8d/unified-advertisement-management-ui';
+
+export const router = createBrowserRouter([
+  {
+    path: '/tenant',
+    element: <ProtectedRoute><MainLayout /></ProtectedRoute>,
+    children: [
+      {
+        path: 'unified-advertisements',
+        element: <UnifiedAdvertisementManagement />,
+      },
+      // 需要添加文件管理路由
+    ],
+  },
+]);
+```
+
+**菜单配置** [Source: web/src/client/tenant/menu.tsx]:
+```typescript
+import { Megaphone } from 'lucide-react';
+
+const menuItems: MenuItem[] = [
+  {
+    key: 'unified-advertisements',
+    label: '广告管理',
+    icon: <Megaphone className="h-4 w-4" />,
+    path: '/tenant/unified-advertisements',
+  },
+  // 需要添加文件管理菜单
+];
+```
+
+**需要添加**:
+```typescript
+import { FileManagement } from '@d8d/unified-file-management-ui';
+import { FileText } from 'lucide-react';
+
+// 路由
+{
+  path: 'files',
+  element: <FileManagement />,
+}
+
+// 菜单
+{
+  key: 'unified-files',
+  label: '文件管理',
+  icon: <FileText className="h-4 w-4" />,
+  path: '/tenant/files',
+}
+```
+
+### 测试要求
+
+**统一广告模块回归测试** [Source: docs/architecture/backend-module-package-standards.md]:
+```bash
+cd packages/unified-advertisements-module
+pnpm test
+```
+
+**统一广告管理UI回归测试** [Source: docs/architecture/ui-package-standards.md]:
+```bash
+cd packages/unified-advertisement-management-ui
+pnpm test
+```
+
+**E2E测试** [Source: docs/architecture/testing-strategy.md]:
+- 使用 Playwright 进行端到端测试
+- 测试文件位置:`web/tests/e2e/`
+- 运行命令:`pnpm test:e2e:chromium`
+
+### 技术约束
+
+**认证中间件** [Source: docs/prd/epic-010-unified-ad-management.md]:
+- 统一文件模块使用 `tenantAuthMiddleware`(仅超级管理员ID=1可访问)
+- 租户后台本身就是超级管理员专用界面
+
+**API路径规范** [Source: docs/architecture/backend-module-package-standards.md]:
+- 模块内路由使用相对路径(`/` 和 `/[:id]`)
+- Server注册时添加完整前缀(`/api/v1/admin/unified-files`)
+
+**RPC类型推断** [Source: docs/architecture/coding-standards.md]:
+- UI包必须使用RPC推断类型,不直接导入schema类型
+- 避免 Date/string 类型不匹配问题
+
+## Change Log
+
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2026-01-04 | 1.1 | 批准故事 | Bob (Scrum Master) |
+| 2026-01-04 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
+
+## Dev Agent Record
+
+### Agent Model Used
+_待开发代理填写_
+
+### Debug Log References
+_待开发代理填写_
+
+### Completion Notes List
+_待开发代理填写_
+
+### File List
+_待开发代理填写_
+
+## QA Results
+_QA代理待填写_