# 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 - [x] **任务1: 创建包结构和配置文件** (AC: 1) - [x] 创建 `packages/unified-advertisements-module` 目录 - [x] 创建 `package.json`,配置包名为 `@d8d/unified-advertisements-module` - [x] 创建 `tsconfig.json` - [x] 创建 `vitest.config.ts`(设置 `fileParallelism: false`) - [x] 创建 `src/` 子目录:`entities/`, `services/`, `routes/`, `schemas/` - [x] 创建 `tests/` 子目录:`integration/`, `utils/` - [x] **任务2: 定义Entity(无tenant_id字段)** (AC: 2) - [x] 创建 `src/entities/unified-advertisement.entity.ts`,参考 `advertisements-module-mt` 但移除 `tenant_id` 字段 - [x] 创建 `src/entities/unified-advertisement-type.entity.ts` - [x] 配置Entity关联:`@ManyToOne` 关联 `FileMt`(使用核心包路径)和 `AdvertisementType` - [x] 创建 `src/entities/index.ts` 导出所有Entity - [x] **任务3: 实现Service层** (AC: 2, 7) - [x] 创建 `src/services/unified-advertisement.service.ts`,继承 `GenericCrudService` - [x] 创建 `src/services/unified-advertisement-type.service.ts` - [x] 覆盖 `create`、`update`、`delete` 方法(使用 `override` 关键字) - [x] 实现软删除逻辑(设置 `status=0`) - [x] 创建 `src/services/index.ts` 导出所有Service - [x] **任务4: 定义Schema** (AC: 5) - [x] 创建 `src/schemas/unified-advertisement.schema.ts`,使用 Zod + OpenAPI装饰器 - [x] 创建 `src/schemas/unified-advertisement-type.schema.ts` - [x] 使用 `z.coerce.date()` 和 `z.coerce.number()` 泛型语法 - [x] 定义 `Create*Dto`、`Update*Dto`、`*ListResponseSchema` - [x] 不导出推断类型(`z.infer`),类型由RPC自动推断 - [x] 创建 `src/schemas/index.ts` 导出所有Schema - [x] **任务5: 实现管理员路由(超级管理员专用)** (AC: 3) - [x] 创建 `src/routes/admin/unified-advertisements.admin.routes.ts` - [x] 使用 `OpenAPIHono` 和 `AuthContext` 泛型 - [x] 使用 `createRoute` 定义路由,包含请求/响应Schema - [x] 应用 `tenantAuthMiddleware` 中间件(来自 `@d8d/tenant-module-mt`,独立包) - [x] 自定义路由使用 `parseWithAwait` 验证响应数据 - [x] 使用 `createZodErrorResponse` 处理Zod错误 - [x] 400响应使用 `ZodErrorSchema`,其他错误使用 `ErrorSchema` - [x] **任务6: 实现用户展示路由(与原模块保持一致)** (AC: 4, 5) - [x] 创建 `src/routes/unified-advertisements.routes.ts` - [x] 使用 `authMiddleware` 中间件(来自 `@d8d/core-module-mt/auth-module-mt`) - [x] 路由结构与原模块完全一致:`GET /api/v1/advertisements`、`GET /api/v1/advertisements/:id` - [x] Schema响应结构与原 `advertisements-module-mt` 一致 - [x] 不使用 `tenantOptions`,返回统一数据给所有租户 - [x] **任务7: 创建包导出入口** (AC: 1) - [x] 创建 `src/index.ts`,导出Entities、Services、Routes、Schemas - [x] 配置 `package.json` 的 `exports` 字段,支持子路径导出 - [x] **任务8: 编写单元测试** (AC: 7) - [x] 创建 `tests/utils/test-data-factory.ts` - [x] 创建Service层单元测试 - [x] 创建Schema验证测试 - [x] 使用时间戳保证测试数据唯一性 - [x] **任务9: 编写集成测试** (AC: 7) - [x] 创建 `tests/integration/unified-advertisements.integration.test.ts` - [x] 测试管理员CRUD操作(验证 `tenantAuthMiddleware` 权限) - [x] 测试用户展示接口(验证返回统一数据) - [x] 测试API响应结构与原模块一致 - [x] **任务10: 代码质量检查** (AC: 1, 7) - [x] 运行 `pnpm typecheck` 确保无TypeScript错误 - [x] 运行 `pnpm lint` 确保代码符合规范 - [x] 运行 `pnpm test` 确保所有测试通过 - [x] 运行 `pnpm test:coverage` 确保覆盖率达标 - [x] **任务11: 添加广告类型管理路由测试** (测试覆盖率提升) - [x] 添加广告类型管理员路由测试(CRUD + 权限验证) - [x] 添加广告类型用户展示路由测试 - [x] 验证类型与广告的关联查询 ## 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类型 z.coerce.number() // 转换为number类型 // ❌ 错误:不指定泛型(Zod 4.0中类型推断可能不准确) z.coerce.date() z.coerce.number() ``` **不导出推断类型** [Source: docs/architecture/backend-module-package-standards.md#类型使用说明]: - Schema只用于请求参数验证和响应定义 - **不需要导出推断的TypeScript类型**(`z.infer`) - 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) | | 2026-01-03 | 1.1 | 完成任务11:添加广告类型管理路由测试 | James (Claude Code) | ## Dev Agent Record ### Agent Model Used claude-opus-4-5-20251101 (d8d-model) ### Debug Log References 无需调试日志(测试问题直接定位并修复) ### Completion Notes List 1. **测试修复 (2026-01-02)**: 修复了Service层关键词搜索功能 - 问题:`GenericCrudService.getList` 需要显式传入 `searchFields` 参数才能进行关键词搜索 - 解决:覆盖 `getList` 方法,指定默认搜索字段 - `UnifiedAdvertisementTypeService`: 搜索 `['name', 'code']` - `UnifiedAdvertisementService`: 搜索 `['title', 'code']` 2. **所有测试通过 (2026-01-02)**: 36/36 测试通过 - 单元测试: 23个 (unified-advertisement: 12, unified-advertisement-type: 11) - 集成测试: 13个 3. **任务11完成 (2026-01-03)**: 添加广告类型管理路由测试 - 添加广告类型管理员路由测试:GET /api/v1/admin/unified-advertisement-types (列表、详情)、POST (创建)、PUT (更新)、DELETE (软删除) - 添加广告类型用户展示路由测试:GET /advertisement-types - 添加类型与广告关联查询测试 - 修复Entity:为`UnifiedAdvertisementType.code`添加唯一索引,添加与`UnifiedAdvertisement`的反向关系 - 最终测试结果:57/57 测试通过 - 单元测试: 23个 - 集成测试: 34个 (包含新增的21个广告类型相关测试) ### File List **包配置文件**: - `package.json` - 包配置 - `tsconfig.json` - TypeScript配置 - `vitest.config.ts` - Vitest测试配置 **Entity层**: - `src/entities/unified-advertisement.entity.ts` - 统一广告Entity(无tenant_id字段) - `src/entities/unified-advertisement-type.entity.ts` - 统一广告类型Entity - `src/entities/index.ts` - Entity导出 **Service层**: - `src/services/unified-advertisement.service.ts` - 广告Service(覆盖getList, create, update, delete) - `src/services/unified-advertisement-type.service.ts` - 广告类型Service(覆盖getList, create, update, delete) - `src/services/index.ts` - Service导出 **Schema层**: - `src/schemas/unified-advertisement.schema.ts` - 广告Schema(使用Zod 4.0语法) - `src/schemas/unified-advertisement-type.schema.ts` - 广告类型Schema - `src/schemas/index.ts` - Schema导出 **路由层**: - `src/routes/admin/unified-advertisements.admin.routes.ts` - 管理员广告路由(使用tenantAuthMiddleware) - `src/routes/admin/unified-advertisement-types.admin.routes.ts` - 管理员广告类型路由 - `src/routes/unified-advertisements.routes.ts` - 用户展示路由(使用authMiddleware) - `src/routes/unified-advertisements.crud.routes.ts` - 广告CRUD路由 - `src/routes/unified-advertisement-types.routes.ts` - 广告类型展示路由 - `src/routes/unified-advertisement-types.crud.routes.ts` - 广告类型CRUD路由 - `src/routes/index.ts` - 路由导出 **包入口**: - `src/index.ts` - 包主入口 **测试文件**: - `tests/utils/test-data-factory.ts` - 测试数据工厂 - `tests/unit/unified-advertisement.service.test.ts` - 广告Service单元测试(12个测试) - `tests/unit/unified-advertisement-type.service.test.ts` - 广告类型Service单元测试(11个测试) - `tests/integration/unified-advertisements.integration.test.ts` - 集成测试(34个测试) - 广告管理员路由测试(7个) - 广告用户展示路由测试(5个) - 广告类型管理员路由测试(13个) - 广告类型用户展示路由测试(3个) - 类型与广告关联查询测试(4个) - API兼容性测试(1个) - 原有测试(13个) ## QA Results _待QA代理填写_