|
|
@@ -0,0 +1,348 @@
|
|
|
+# Story 010.001: 创建统一广告后端模块
|
|
|
+
|
|
|
+## Status
|
|
|
+Approved
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+**As a** 超级管理员,
|
|
|
+**I want** 一个统一的后端广告模块(无租户隔离),
|
|
|
+**so that** 可以在租户管理后台统一管理所有广告,所有租户用户端看到相同的广告数据。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+1. 创建 `packages/unified-advertisements-module` 包,包含完整的Entity、Service、Schema、Routes结构
|
|
|
+2. Entity定义不包含 `tenant_id` 字段,与原 `advertisements-module-mt` 的Entity结构相同但移除租户隔离
|
|
|
+3. 实现管理员路由(使用 `tenantAuthMiddleware`),只有超级管理员(ID=1)可访问
|
|
|
+4. 实现用户展示路由(使用 `authMiddleware`),返回统一的广告数据给所有租户
|
|
|
+5. API路由路径、请求参数、响应结构与原模块保持100%兼容,确保小程序端无感知
|
|
|
+6. 关联文件模块(`@d8d/core-module-mt/file-module-mt`),使用 `FileMt` 实体
|
|
|
+7. 包含完整的单元测试和集成测试
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] **任务1: 创建包结构和配置文件** (AC: 1)
|
|
|
+ - [ ] 创建 `packages/unified-advertisements-module` 目录
|
|
|
+ - [ ] 创建 `package.json`,配置包名为 `@d8d/unified-advertisements-module`
|
|
|
+ - [ ] 创建 `tsconfig.json`
|
|
|
+ - [ ] 创建 `vitest.config.ts`(设置 `fileParallelism: false`)
|
|
|
+ - [ ] 创建 `src/` 子目录:`entities/`, `services/`, `routes/`, `schemas/`
|
|
|
+ - [ ] 创建 `tests/` 子目录:`integration/`, `utils/`
|
|
|
+
|
|
|
+- [ ] **任务2: 定义Entity(无tenant_id字段)** (AC: 2)
|
|
|
+ - [ ] 创建 `src/entities/unified-advertisement.entity.ts`,参考 `advertisements-module-mt` 但移除 `tenant_id` 字段
|
|
|
+ - [ ] 创建 `src/entities/unified-advertisement-type.entity.ts`
|
|
|
+ - [ ] 配置Entity关联:`@ManyToOne` 关联 `FileMt`(使用核心包路径)和 `AdvertisementType`
|
|
|
+ - [ ] 创建 `src/entities/index.ts` 导出所有Entity
|
|
|
+
|
|
|
+- [ ] **任务3: 实现Service层** (AC: 2, 7)
|
|
|
+ - [ ] 创建 `src/services/unified-advertisement.service.ts`,继承 `GenericCrudService`
|
|
|
+ - [ ] 创建 `src/services/unified-advertisement-type.service.ts`
|
|
|
+ - [ ] 覆盖 `create`、`update`、`delete` 方法(使用 `override` 关键字)
|
|
|
+ - [ ] 实现软删除逻辑(设置 `status=0`)
|
|
|
+ - [ ] 创建 `src/services/index.ts` 导出所有Service
|
|
|
+
|
|
|
+- [ ] **任务4: 定义Schema** (AC: 5)
|
|
|
+ - [ ] 创建 `src/schemas/unified-advertisement.schema.ts`,使用 Zod + OpenAPI装饰器
|
|
|
+ - [ ] 创建 `src/schemas/unified-advertisement-type.schema.ts`
|
|
|
+ - [ ] 使用 `z.coerce.date<Date>()` 和 `z.coerce.number<number>()` 泛型语法
|
|
|
+ - [ ] 定义 `Create*Dto`、`Update*Dto`、`*ListResponseSchema`
|
|
|
+ - [ ] 不导出推断类型(`z.infer`),类型由RPC自动推断
|
|
|
+ - [ ] 创建 `src/schemas/index.ts` 导出所有Schema
|
|
|
+
|
|
|
+- [ ] **任务5: 实现管理员路由(超级管理员专用)** (AC: 3)
|
|
|
+ - [ ] 创建 `src/routes/admin/unified-advertisements.admin.routes.ts`
|
|
|
+ - [ ] 使用 `OpenAPIHono` 和 `AuthContext` 泛型
|
|
|
+ - [ ] 使用 `createRoute` 定义路由,包含请求/响应Schema
|
|
|
+ - [ ] 应用 `tenantAuthMiddleware` 中间件(来自 `@d8d/tenant-module-mt`,独立包)
|
|
|
+ - [ ] 自定义路由使用 `parseWithAwait` 验证响应数据
|
|
|
+ - [ ] 使用 `createZodErrorResponse` 处理Zod错误
|
|
|
+ - [ ] 400响应使用 `ZodErrorSchema`,其他错误使用 `ErrorSchema`
|
|
|
+
|
|
|
+- [ ] **任务6: 实现用户展示路由(与原模块保持一致)** (AC: 4, 5)
|
|
|
+ - [ ] 创建 `src/routes/unified-advertisements.routes.ts`
|
|
|
+ - [ ] 使用 `authMiddleware` 中间件(来自 `@d8d/core-module-mt/auth-module-mt`)
|
|
|
+ - [ ] 路由结构与原模块完全一致:`GET /api/v1/advertisements`、`GET /api/v1/advertisements/:id`
|
|
|
+ - [ ] Schema响应结构与原 `advertisements-module-mt` 一致
|
|
|
+ - [ ] 不使用 `tenantOptions`,返回统一数据给所有租户
|
|
|
+
|
|
|
+- [ ] **任务7: 创建包导出入口** (AC: 1)
|
|
|
+ - [ ] 创建 `src/index.ts`,导出Entities、Services、Routes、Schemas
|
|
|
+ - [ ] 配置 `package.json` 的 `exports` 字段,支持子路径导出
|
|
|
+
|
|
|
+- [ ] **任务8: 编写单元测试** (AC: 7)
|
|
|
+ - [ ] 创建 `tests/utils/test-data-factory.ts`
|
|
|
+ - [ ] 创建Service层单元测试
|
|
|
+ - [ ] 创建Schema验证测试
|
|
|
+ - [ ] 使用时间戳保证测试数据唯一性
|
|
|
+
|
|
|
+- [ ] **任务9: 编写集成测试** (AC: 7)
|
|
|
+ - [ ] 创建 `tests/integration/unified-advertisements.integration.test.ts`
|
|
|
+ - [ ] 测试管理员CRUD操作(验证 `tenantAuthMiddleware` 权限)
|
|
|
+ - [ ] 测试用户展示接口(验证返回统一数据)
|
|
|
+ - [ ] 测试API响应结构与原模块一致
|
|
|
+
|
|
|
+- [ ] **任务10: 代码质量检查** (AC: 1, 7)
|
|
|
+ - [ ] 运行 `pnpm typecheck` 确保无TypeScript错误
|
|
|
+ - [ ] 运行 `pnpm lint` 确保代码符合规范
|
|
|
+ - [ ] 运行 `pnpm test` 确保所有测试通过
|
|
|
+ - [ ] 运行 `pnpm test:coverage` 确保覆盖率达标
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### 项目结构信息
|
|
|
+
|
|
|
+**新包位置**:
|
|
|
+```
|
|
|
+packages/unified-advertisements-module/
|
|
|
+├── package.json
|
|
|
+├── tsconfig.json
|
|
|
+├── vitest.config.ts
|
|
|
+├── src/
|
|
|
+│ ├── entities/
|
|
|
+│ │ ├── unified-advertisement.entity.ts
|
|
|
+│ │ ├── unified-advertisement-type.entity.ts
|
|
|
+│ │ └── index.ts
|
|
|
+│ ├── services/
|
|
|
+│ │ ├── unified-advertisement.service.ts
|
|
|
+│ │ ├── unified-advertisement-type.service.ts
|
|
|
+│ │ └── index.ts
|
|
|
+│ ├── routes/
|
|
|
+│ │ ├── admin/
|
|
|
+│ │ │ └── unified-advertisements.admin.routes.ts
|
|
|
+│ │ ├── unified-advertisements.routes.ts
|
|
|
+│ │ ├── unified-advertisement-types.routes.ts
|
|
|
+│ │ └── index.ts
|
|
|
+│ ├── schemas/
|
|
|
+│ │ ├── unified-advertisement.schema.ts
|
|
|
+│ │ ├── unified-advertisement-type.schema.ts
|
|
|
+│ │ └── index.ts
|
|
|
+│ └── index.ts
|
|
|
+└── tests/
|
|
|
+ ├── integration/
|
|
|
+ │ └── unified-advertisements.integration.test.ts
|
|
|
+ └── utils/
|
|
|
+ └── test-data-factory.ts
|
|
|
+```
|
|
|
+
|
|
|
+**参考模块**:
|
|
|
+- 原多租户广告模块: `packages/advertisements-module-mt`
|
|
|
+- 认证模块: `@d8d/core-module-mt/auth-module-mt`
|
|
|
+- 租户模块: `@d8d/core-module-mt/tenant-module-mt`
|
|
|
+- 文件模块: `@d8d/core-module-mt/file-module-mt`
|
|
|
+
|
|
|
+### Entity设计规范
|
|
|
+
|
|
|
+**统一广告Entity** (`unified-advertisement.entity.ts`):
|
|
|
+- 继承自原 `advertisements-module-mt` 的Entity结构
|
|
|
+- **关键区别**: 无 `tenant_id` 字段
|
|
|
+- 使用 `@ManyToOne` 关联 `FileMt`(从核心包路径引用)
|
|
|
+- 使用 `@ManyToOne` 关联 `UnifiedAdvertisementType`
|
|
|
+- 字段包括:`id`, `title`, `typeId`, `code`, `url`, `imageFileId`, `sort`, `status`, `actionType`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`
|
|
|
+
|
|
|
+**核心包引用规范** [Source: docs/architecture/backend-module-package-standards.md#核心包引用规范]:
|
|
|
+```typescript
|
|
|
+// ✅ 正确:从核心包路径引用
|
|
|
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
|
|
|
+
|
|
|
+// ❌ 错误:直接从桥接包引用
|
|
|
+import { FileMt } from '@d8d/file-module-mt';
|
|
|
+```
|
|
|
+
|
|
|
+### 路由设计规范
|
|
|
+
|
|
|
+**管理员接口** (新增):
|
|
|
+```
|
|
|
+GET /api/v1/admin/unified-advertisements # 广告列表
|
|
|
+POST /api/v1/admin/unified-advertisements # 创建广告
|
|
|
+PUT /api/v1/admin/unified-advertisements/:id # 更新广告
|
|
|
+DELETE /api/v1/admin/unified-advertisements/:id # 删除广告
|
|
|
+
|
|
|
+GET /api/v1/admin/unified-advertisement-types # 广告类型列表
|
|
|
+POST /api/v1/admin/unified-advertisement-types # 创建广告类型
|
|
|
+PUT /api/v1/admin/unified-advertisement-types/:id # 更新广告类型
|
|
|
+DELETE /api/v1/admin/unified-advertisement-types/:id # 删除广告类型
|
|
|
+```
|
|
|
+
|
|
|
+**用户展示接口** (保持不变 - 小程序端使用):
|
|
|
+```
|
|
|
+GET /api/v1/advertisements # 获取有效广告列表(不变)
|
|
|
+GET /api/v1/advertisements/:id # 获取单个广告详情(不变)
|
|
|
+GET /api/v1/advertisement-types # 获取广告类型列表(不变)
|
|
|
+```
|
|
|
+
|
|
|
+**关键设计**: 小程序端无需任何改动,API契约100%兼容。只是后端数据源从多租户切换到统一模块。
|
|
|
+
|
|
|
+### 中间件使用规范
|
|
|
+
|
|
|
+**管理员路由**:
|
|
|
+- 使用 `tenantAuthMiddleware`(来自 `@d8d/tenant-module-mt`,独立包)
|
|
|
+- 只有超级管理员(ID=1)可访问
|
|
|
+- 代码引用:`import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';`
|
|
|
+- 参考: `packages/tenant-module-mt/src/middleware/`
|
|
|
+
|
|
|
+**用户展示路由**:
|
|
|
+- 使用 `authMiddleware`(来自 `@d8d/core-module-mt/auth-module-mt`)
|
|
|
+- 进行多租户认证,但返回统一数据(无tenant_id过滤)
|
|
|
+- 代码引用:`import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';`
|
|
|
+
|
|
|
+### Schema规范
|
|
|
+
|
|
|
+**Zod 4.0 coerce使用** [Source: docs/architecture/backend-module-package-standards.md#Schema规范]:
|
|
|
+```typescript
|
|
|
+// ✅ 正确:Zod 4.0 - 使用泛型指定类型
|
|
|
+z.coerce.date<Date>() // 转换为Date类型
|
|
|
+z.coerce.number<number>() // 转换为number类型
|
|
|
+
|
|
|
+// ❌ 错误:不指定泛型(Zod 4.0中类型推断可能不准确)
|
|
|
+z.coerce.date()
|
|
|
+z.coerce.number()
|
|
|
+```
|
|
|
+
|
|
|
+**不导出推断类型** [Source: docs/architecture/backend-module-package-standards.md#类型使用说明]:
|
|
|
+- Schema只用于请求参数验证和响应定义
|
|
|
+- **不需要导出推断的TypeScript类型**(`z.infer<typeof Schema>`)
|
|
|
+- UI包通过RPC直接从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 // 避免数据库连接冲突
|
|
|
+ }
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+**测试数据工厂** [Source: docs/architecture/backend-module-package-standards.md#测试数据工厂]:
|
|
|
+- 使用时间戳保证数据唯一性
|
|
|
+- 提供 `createTestData(overrides)` 静态方法
|
|
|
+- 提供 `createTestRecord(dataSource, overrides)` 异步方法
|
|
|
+
|
|
|
+### API兼容性要求
|
|
|
+
|
|
|
+**关键**: 与原 `advertisements-module-mt` 保持100%API兼容
|
|
|
+- 路由路径完全相同
|
|
|
+- 请求参数结构相同
|
|
|
+- 响应Schema结构相同
|
|
|
+- 小程序端无需任何改动
|
|
|
+
|
|
|
+### 数据库类型映射
|
|
|
+
|
|
|
+[Source: docs/architecture/backend-module-package-standards.md#数据库类型规范]
|
|
|
+
|
|
|
+| 数据库类型 | TypeORM类型 | 备注 |
|
|
|
+|------------|-------------|------|
|
|
|
+| `int unsigned` | `int` + `unsigned: true` | 主键常用 |
|
|
|
+| `varchar(n)` | `varchar` + `length: n` | 字符串 |
|
|
|
+| `timestamp` | `timestamp` | 时间戳 |
|
|
|
+| `int` (状态) | `int` | 状态枚举 |
|
|
|
+
|
|
|
+### package.json配置参考
|
|
|
+
|
|
|
+[Source: docs/architecture/backend-module-package-standards.md#包配置规范]
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "name": "@d8d/unified-advertisements-module",
|
|
|
+ "version": "1.0.0",
|
|
|
+ "type": "module",
|
|
|
+ "main": "src/index.ts",
|
|
|
+ "types": "src/index.ts",
|
|
|
+ "scripts": {
|
|
|
+ "test": "vitest run",
|
|
|
+ "test:coverage": "vitest run --coverage",
|
|
|
+ "typecheck": "tsc --noEmit"
|
|
|
+ },
|
|
|
+ "dependencies": {
|
|
|
+ "@d8d/shared-crud": "workspace:*",
|
|
|
+ "@d8d/shared-types": "workspace:*",
|
|
|
+ "@d8d/shared-utils": "workspace:*",
|
|
|
+ "@d8d/core-module-mt": "workspace:*",
|
|
|
+ "@d8d/tenant-module-mt": "workspace:*",
|
|
|
+ "@hono/zod-openapi": "^1.0.2",
|
|
|
+ "typeorm": "^0.3.20",
|
|
|
+ "zod": "^4.1.12"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**重要说明**:
|
|
|
+- **`@d8d/core-module-mt`**: 核心包聚合了 auth-module-mt、file-module-mt、user-module-mt 等基础模块
|
|
|
+ - 代码引用子模块:`import { FileMt } from '@d8d/core-module-mt/file-module-mt';`
|
|
|
+ - 代码引用子模块:`import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';`
|
|
|
+- **`@d8d/tenant-module-mt`**: 独立的租户管理模块包(不在core-module-mt中)
|
|
|
+ - 代码引用:`import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';`
|
|
|
+
|
|
|
+## Testing
|
|
|
+
|
|
|
+### 测试文件位置
|
|
|
+- 单元测试: `tests/unit/`(与源码文件对应)
|
|
|
+- 集成测试: `tests/integration/`
|
|
|
+- 测试工具: `tests/utils/`
|
|
|
+
|
|
|
+### 测试框架
|
|
|
+- **Vitest**: 主要测试运行器
|
|
|
+- **hono/testing**: API路由测试
|
|
|
+- **TypeORM**: 数据库测试
|
|
|
+
|
|
|
+### 测试标准
|
|
|
+[Source: docs/architecture/testing-strategy.md]
|
|
|
+
|
|
|
+| 测试类型 | 最低要求 | 目标要求 |
|
|
|
+|----------|----------|----------|
|
|
|
+| 单元测试 | 70% | 80% |
|
|
|
+| 集成测试 | 50% | 60% |
|
|
|
+
|
|
|
+### 关键测试要求
|
|
|
+1. **API兼容性测试**: 验证响应结构与原模块完全一致
|
|
|
+2. **权限测试**: 验证管理员路由只有超级管理员可访问
|
|
|
+3. **统一数据测试**: 验证不同租户用户获取到相同的广告数据
|
|
|
+4. **软删除测试**: 验证删除操作设置 `status=0` 而非物理删除
|
|
|
+5. **关联测试**: 验证 `FileMt` 和 `AdvertisementType` 关联正确
|
|
|
+
|
|
|
+### 测试执行命令
|
|
|
+```bash
|
|
|
+# 进入模块目录
|
|
|
+cd packages/unified-advertisements-module
|
|
|
+
|
|
|
+# 运行所有测试
|
|
|
+pnpm test
|
|
|
+
|
|
|
+# 运行集成测试
|
|
|
+pnpm test:integration
|
|
|
+
|
|
|
+# 生成覆盖率报告
|
|
|
+pnpm test:coverage
|
|
|
+
|
|
|
+# 类型检查
|
|
|
+pnpm typecheck
|
|
|
+```
|
|
|
+
|
|
|
+## Change Log
|
|
|
+
|
|
|
+| Date | Version | Description | Author |
|
|
|
+|------|---------|-------------|--------|
|
|
|
+| 2026-01-02 | 1.0 | 初始故事创建 | Bob (Scrum Master) |
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+_待开发代理填写_
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+_待开发代理填写_
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+_待开发代理填写_
|
|
|
+
|
|
|
+### File List
|
|
|
+_待开发代理填写_
|
|
|
+
|
|
|
+## QA Results
|
|
|
+_待QA代理填写_
|