Approved
As a 开发者, I want 从单租户文件模块复制并改造创建统一文件后端模块(unified-file-module), so that 统一广告模块可以使用无租户隔离的文件实体,确保架构一致性。
packages/unified-file-module 包(使用 cp -r 命令直接复制 packages/file-module 整个文件夹)@d8d/file-module 改为 @d8d/unified-file-module),移除用户模块和认证模块依赖tenantAuthMiddleware,只有超级管理员可访问,不需要用户展示路由)[ ] 任务1: 复制文件模块创建统一文件模块 (AC: 1)
cp -r packages/file-module packages/unified-file-module 命令直接复制整个文件夹[ ] 任务2: 修改包配置文件 (AC: 2)
package.json 包名:@d8d/file-module → @d8d/unified-file-modulepackage.json 描述:添加"unified"相关描述package.json 中移除 @d8d/user-module 和 @d8d/auth-module(本模块只在超级管理员后台使用,不需要用户和认证依赖)@d8d/tenant-module-mt(需要使用 tenantAuthMiddleware)[ ] 任务3: 修改模块导出文件 (AC: 2)
src/index.ts 导出的实体名:File → UnifiedFilesrc/index.ts 导出的服务名:FileService → UnifiedFileService,MinioService 保持不变src/entities/index.ts 导出[ ] 任务4: 修改Entity定义 (AC: 3)
entities/file.entity.ts → entities/unified-file.entity.tsexport class File → export class UnifiedFiletenant_id 字段(单租户模块本身就没有,验证确认即可)@Entity('file') → @Entity('unified_file')@ManyToOne('UserEntity') 和 uploadUser 字段(因为不需要用户模块依赖)uploadUserId 字段不变(只保留ID,不关联UserEntity)[ ] 任务5: 修改Service层 (AC: 4)
services/file.service.ts → services/unified-file.service.tsexport class FileService → export class UnifiedFileServiceFile → UnifiedFileMinioService 保持不变(复用原有实现)[ ] 任务6: 修改Schema定义 (AC: 4)
schemas/file.schema.ts 中所有Schema名称:FileSchema → UnifiedFileSchema,CreateFileSchema → CreateUnifiedFileSchema,等等[ ] 任务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.tsFile → UnifiedFileFileService → UnifiedFileService[ ] 任务9: 编写集成测试 (AC: 6, 7)
tests/integration/file.integration.test.ts → tests/integration/unified-file.integration.test.ts[ ] 任务10: 类型检查和代码质量 (AC: 8)
pnpm typecheck 确保无TypeScript类型错误pnpm test 确保所有测试通过pnpm test:coverage 确保测试覆盖率达到70%以上史诗010的统一设计模式:
tenantAuthMiddleware(只有超级管理员ID=1可访问)authMiddleware(不需要用户展示路由)tenant_id 字段/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 字段,是多租户隔离的模块结构 [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]:
@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]:
{
"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 命令直接复制
根据用户明确要求,必须使用以下命令直接复制整个文件模块文件夹:
cp -r packages/file-module packages/unified-file-module
为什么使用 cp 命令直接复制:
复制后的修改清单:
@d8d/file-module → @d8d/unified-file-module@d8d/user-module 和 @d8d/auth-module,添加 @d8d/tenant-module-mtFile → UnifiedFile,表名 file → unified_fileUserEntity 关联,只保留 uploadUserId 字段FileService → UnifiedFileServiceUnified 前缀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: truetimestamp 类型,默认值为 () => '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>() 泛型语法⚠️ 重要: 新模块必须从正确路径引用基础模块
| 模块类型 | ✅ 正确引用 | 说明 |
|---|---|---|
| 租户模块(多租户) | @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配置 [Source: docs/architecture/tech-stack.md]:
d8dai环境变量:
MINIO_HOST: MinIO服务器地址MINIO_PORT: MinIO端口MINIO_USE_SSL: 是否使用SSLMINIO_BUCKET_NAME: 存储桶名称新模块位置: packages/unified-file-module/ [Source: docs/architecture/source-tree.md]
测试标准: Vitest [Source: docs/architecture/tech-stack.md]
测试文件位置 [Source: docs/architecture/backend-module-package-standards.md]:
packages/unified-file-module/tests/unit/packages/unified-file-module/tests/integration/测试框架:
测试配置 [Source: docs/architecture/backend-module-package-standards.md]:
// 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 // 避免数据库连接冲突
}
});
测试覆盖要求:
测试命令:
# 进入模块目录
cd packages/unified-file-module
# 运行所有测试
pnpm test
# 运行单元测试
pnpm test:unit
# 运行集成测试
pnpm test:integration
# 生成覆盖率报告
pnpm test:coverage
# 类型检查
pnpm typecheck
| 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) |
开发代理实施时填写
开发代理实施时填写
开发代理实施时填写
开发代理实施时填写
QA代理待填写