|
|
@@ -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代理待填写_
|