010.001.story.md 17 KB

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)

    • 创建 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/
  • [x] 任务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
  • [x] 任务3: 实现Service层 (AC: 2, 7)

    • 创建 src/services/unified-advertisement.service.ts,继承 GenericCrudService
    • 创建 src/services/unified-advertisement-type.service.ts
    • 覆盖 createupdatedelete 方法(使用 override 关键字)
    • 实现软删除逻辑(设置 status=0
    • 创建 src/services/index.ts 导出所有Service
  • [x] 任务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*DtoUpdate*Dto*ListResponseSchema
    • 不导出推断类型(z.infer),类型由RPC自动推断
    • 创建 src/schemas/index.ts 导出所有Schema
  • [x] 任务5: 实现管理员路由(超级管理员专用) (AC: 3)

    • 创建 src/routes/admin/unified-advertisements.admin.routes.ts
    • 使用 OpenAPIHonoAuthContext 泛型
    • 使用 createRoute 定义路由,包含请求/响应Schema
    • 应用 tenantAuthMiddleware 中间件(来自 @d8d/tenant-module-mt,独立包)
    • 自定义路由使用 parseWithAwait 验证响应数据
    • 使用 createZodErrorResponse 处理Zod错误
    • 400响应使用 ZodErrorSchema,其他错误使用 ErrorSchema
  • [x] 任务6: 实现用户展示路由(与原模块保持一致) (AC: 4, 5)

    • 创建 src/routes/unified-advertisements.routes.ts
    • 使用 authMiddleware 中间件(来自 @d8d/core-module-mt/auth-module-mt
    • 路由结构与原模块完全一致:GET /api/v1/advertisementsGET /api/v1/advertisements/:id
    • Schema响应结构与原 advertisements-module-mt 一致
    • 不使用 tenantOptions,返回统一数据给所有租户
  • [x] 任务7: 创建包导出入口 (AC: 1)

    • 创建 src/index.ts,导出Entities、Services、Routes、Schemas
    • 配置 package.jsonexports 字段,支持子路径导出
  • [x] 任务8: 编写单元测试 (AC: 7)

    • 创建 tests/utils/test-data-factory.ts
    • 创建Service层单元测试
    • 创建Schema验证测试
    • 使用时间戳保证测试数据唯一性
  • [x] 任务9: 编写集成测试 (AC: 7)

    • 创建 tests/integration/unified-advertisements.integration.test.ts
    • 测试管理员CRUD操作(验证 tenantAuthMiddleware 权限)
    • 测试用户展示接口(验证返回统一数据)
    • 测试API响应结构与原模块一致
  • [x] 任务10: 代码质量检查 (AC: 1, 7)

    • 运行 pnpm typecheck 确保无TypeScript错误
    • 运行 pnpm lint 确保代码符合规范
    • 运行 pnpm test 确保所有测试通过
    • 运行 pnpm test:coverage 确保覆盖率达标
  • [x] 任务11: 添加广告类型管理路由测试 (测试覆盖率提升)

    • 添加广告类型管理员路由测试(CRUD + 权限验证)
    • 添加广告类型用户展示路由测试
    • 验证类型与广告的关联查询

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#核心包引用规范]:

// ✅ 正确:从核心包路径引用
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规范]:

// ✅ 正确: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#测试配置]:

// 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#包配置规范]

{
  "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. 关联测试: 验证 FileMtAdvertisementType 关联正确

测试执行命令

# 进入模块目录
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代理填写