# 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<Date>()` 和 `z.coerce.number<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>()       // 转换为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) |
| 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代理填写_
