epic-010-unified-ad-management.md 37 KB

史诗 010: 统一广告管理系统 - Brownfield Enhancement

版本信息

版本 日期 描述 作者
1.0 2026-01-02 初始版本 James (Claude Code)
1.1 2026-01-03 完成任务11:添加广告类型管理路由测试 James (Claude Code)
1.2 2026-01-03 添加故事010.003:修复路由路径规范问题 James (Claude Code)
1.3 2026-01-03 添加故事010.004:修复路由参数类型规范问题 James (Claude Code)
1.4 2026-01-03 完成故事010.004:修复路由参数类型规范问题 James (Claude Code)
1.5 2026-01-03 更新故事010.002状态为Ready for Review James (Claude Code)
1.6 2026-01-03 添加故事010.005:补充测试覆盖度 James (Claude Code)
1.7 2026-01-03 完成故事010.005:补充测试覆盖度(51个测试,覆盖率87.33%) Claude Code (Happy)
1.8 2026-01-03 完成故事010.006:Web集成和Server模块替换 James (Claude Code)
1.9 2026-01-03 修复故事010.006集成测试:全部17个测试通过 James (Claude Code)
1.10 2026-01-03 修复故事010.006 E2E测试:50个测试通过,更新测试策略文档 James (Claude Code)
1.11 2026-01-03 添加故事010.007:租户后台UI交互E2E测试 James (Claude Code)
1.12 2026-01-03 添加故事010.008:小程序端广告展示E2E测试 James (Claude Code)
1.13 2026-01-03 添加故事010.009-011:拆分统一文件模块为三个故事 James (Claude Code)
1.14 2026-01-04 批准故事010.009:创建统一文件后端模块 Bob (Scrum Master)
1.15 2026-01-04 完成故事010.009:创建统一文件后端模块(22个测试,覆盖率59.47%) Claude (Dev Agent)
1.16 2026-01-04 批准故事010.010:创建统一文件管理UI包 Bob (Scrum Master)
1.17 2026-01-04 完成故事010.010:创建统一文件管理UI包(30个测试,使用RPC推断类型) Claude (Dev Agent)
1.18 2026-01-04 批准故事010.011:集成统一文件模块到统一广告和租户后台 Bob (Scrum Master)
1.19 2026-01-04 完成故事010.011:集成统一文件模块(统一广告模块迁移到UnifiedFile) Claude (Dev Agent)
1.20 2026-01-04 添加故事010.012:统一广告模块响应格式规范化 James (Dev Agent)

史诗目标

将多租户广告管理改造为统一广告管理系统,由超级管理员在租户管理后台统一配置广告,所有租户共享相同的广告数据展示。同时保持小程序端广告读取逻辑不受影响。

史诗描述

现有系统上下文

  • 当前相关功能: 系统使用 @d8d/advertisements-module-mt 多租户广告模块,每个租户在自己的admin后台独立管理广告,数据通过 tenant_id 字段隔离
  • 技术栈: Hono + TypeORM + PostgreSQL + React + Vite
  • 集成点:
    • Server包: packages/server/src/index.ts 注册广告路由
    • Admin后台: web/src/client/admin/ 使用 @d8d/advertisement-management-ui-mt
    • 小程序端: 通过 /api/v1/advertisements 读取广告

增强详情

添加/修改内容:

  1. 创建 packages/unified-advertisements-module - 统一广告模块(无tenant_id隔离)
  2. 创建 packages/unified-advertisement-management-ui - 统一广告管理UI包
  3. 租户后台集成广告管理功能(web/src/client/tenant/
  4. 从各租户admin后台移除广告管理功能
  5. Server包替换模块引用 - 保持API路由和契约不变,仅切换后端模块

集成方式:

  • 管理员接口: 使用 tenantAuthMiddleware(超级管理员专用,参考租户模块实现)
  • 用户展示接口: 使用 authMiddleware(多租户认证,但返回统一的广告数据)
  • 数据库: 新建无 tenant_id 字段的表结构
  • 关键设计: API路由路径、请求参数、响应结构完全不变,小程序端无感知

成功标准:

  • 租户后台可完整管理广告(增删改查)
  • 所有租户用户端读取到统一的广告数据
  • Admin后台不再显示广告管理入口
  • 小程序端无需重新发布,API兼容性100%

用户故事

Story 1: 创建统一广告模块 ✅ 已完成

标题: 创建统一广告后端模块 (unified-advertisements-module)

描述: 复制单租户广告模块并改造,移除tenant_id字段,区分管理员和用户接口

任务:

  • 创建包结构和配置文件
  • 定义Entity(无tenant_id字段)
  • 实现Service层
  • 定义Schema
  • 实现管理员路由(使用tenantAuthMiddleware)
  • 实现用户展示路由(使用authMiddleware)
  • 编写单元测试和集成测试
  • 添加广告类型管理路由测试(测试覆盖率提升 - 2026-01-03完成)
    • 添加广告类型管理员路由测试(CRUD + 权限验证)
    • 添加广告类型用户展示路由测试
    • 验证类型与广告的关联查询

完成日期: 2026-01-02(初始),2026-01-03(任务11) 测试覆盖: 57个测试全部通过(23个单元测试 + 34个集成测试) 相关文件: docs/stories/010.001.story.md

Story 2: 创建统一广告管理UI ✅ 已完成

标题: 创建统一广告管理UI包 (unified-advertisement-management-ui)

描述: 复制单租户广告管理UI并改造,API端点指向统一模块

任务:

  • 创建UI包结构
  • 实现广告管理组件(列表、创建、编辑、删除)
  • 实现广告类型管理组件
  • 创建API客户端(指向统一模块端点)
  • 编写组件测试

完成日期: 2026-01-03 测试覆盖: 13个集成测试全部通过 相关文件: docs/stories/010.002.story.md

Story 3: 修复路由路径规范问题 ✅ 已完成

标题: 修复统一广告模块路由路径规范问题

描述: 故事010.001实施时违反了后端模块开发规范,在模块路由定义中添加了/api/v1前缀。按照规范,该前缀应该在server包注册路由时添加,而非模块内部。

问题说明:

  • 错误: 模块路由路径定义为 /api/v1/admin/unified-advertisements
  • 正确: 模块路由路径应为 /(相对路径,由server包添加完整前缀)
  • 原因: /api/v1/admin 前缀由server包在注册时统一添加,模块内部只需定义相对路径

任务:

  • 修复 unified-advertisements.admin.routes.ts 路由路径(改为 //:id
  • 修复 unified-advertisement-types.admin.routes.ts 路由路径(改为 //:id
  • 修复 unified-advertisement-types.routes.ts 用户路由路径(改为 /
  • 更新集成测试文件中的API路径调用方式
  • 更新史诗010文档

修复内容:

  1. 管理员广告路由: /api/v1/admin/unified-advertisements/
  2. 管理员广告路由: /api/v1/admin/unified-advertisements/:id/:id
  3. 管理员广告类型路由: /api/v1/admin/unified-advertisement-types/
  4. 管理员广告类型路由: /api/v1/admin/unified-advertisement-types/:id/:id
  5. 用户广告类型路由: /api/v1/advertisement-types/
  6. 集成测试: 更新测试调用方式(adminClient.$get() 而非 adminClient['/path'].$get()

完成日期: 2026-01-03 相关文件: docs/stories/010.003.story.md

Story 4: 修复路由参数类型规范问题 ✅ 已完成

标题: 修复统一广告模块路由参数类型规范问题

描述: 统一广告模块的路由定义中缺少 params schema 定义,导致 RPC 客户端推断出的 :id 参数类型为 string,而不是 number。需要在路由 schema 中添加 params 定义,使用 z.coerce.number() 进行类型转换。

问题说明:

  • 错误: 路由没有定义 request.params,导致 RPC 客户端推断 :idstring 类型
  • 正确: 使用 z.coerce.number<number>() 定义 params,自动转换 string 到 number
  • 原因: 参照 createCrudRoutes 的开发规范,所有路径参数都应明确定义类型

任务:

  • 修复 unified-advertisements.admin.routes.ts 的 params 定义(getRoute, updateRoute, deleteRoute)
  • 修复 unified-advertisement-types.admin.routes.ts 的 params 定义
  • 更新 UI 包移除类型转换(直接传递 number)
  • 验证集成测试通过

修复内容:

  1. getRoute: 添加 request.params 定义,使用 z.coerce.number<number>()
  2. updateRoute: 添加 request.params 定义
  3. deleteRoute: 添加 request.params 定义
  4. 前端 UI: 移除 String(id) 类型转换
  5. 路由处理函数: 使用 c.req.valid('param') 替代 parseInt(c.req.param('id'))

完成日期: 2026-01-03 测试结果: 57/57 测试通过 相关文件: docs/stories/010.004.story.md

Story 5: 补充测试覆盖度 ✅ 已完成

标题: 补充统一广告管理UI包测试覆盖度

描述: 为统一广告管理UI包补充缺失的测试场景,提升测试覆盖率到70%以上,确保代码质量和稳定性。

背景说明:

  • 故事010.002实施时完成了基础的CRUD测试(13个测试通过)
  • 但存在以下测试场景未覆盖:
    • API错误处理(网络失败、服务器错误、业务错误)
    • 表单验证失败(必填字段、格式验证、长度限制)
    • 分页功能(页码切换、边界条件)
    • 编辑表单状态切换、选择器交互、图片选择器交互
  • 当前覆盖率约60-70%,需要补充测试达到70%以上

任务:

  • 创建API错误处理测试(网络、500、400/404/409)- 5个测试
  • 创建表单验证测试(必填字段、格式、长度)- 8个测试
  • 创建分页功能测试(页码切换、边界条件)- 6个测试
  • 创建编辑表单状态切换测试 - 7个测试
  • 创建广告类型选择器交互测试 - 7个测试
  • 创建图片选择器交互测试 - 5个测试
  • 更新测试覆盖率配置(阈值配置)
  • 代码质量检查(覆盖率达标、类型检查通过)

完成日期: 2026-01-03 测试成果: 新增 51 个集成测试,累计 64 个测试全部通过 测试覆盖率: 87.33% statements, 67.85% branches, 81.44% functions, 90.68% lines 相关文件: docs/stories/010.005.story.md

新增测试文件:

  • tests/integration/error-handling.integration.test.tsx - API 错误处理测试
  • tests/integration/form-validation.integration.test.tsx - 表单验证测试
  • tests/integration/pagination.integration.test.tsx - 分页功能测试
  • tests/integration/edit-form-state.integration.test.tsx - 编辑表单状态测试
  • tests/integration/ad-type-selector.integration.test.tsx - 广告类型选择器测试
  • tests/integration/file-selector.integration.test.tsx - 图片选择器测试

Story 6: Web集成和Server模块替换 ✅ 已完成

标题: 集成到租户后台、移除admin后台广告管理、Server切换模块

描述: 将统一广告管理UI集成到租户后台,从admin后台移除广告管理,关键:Server包切换模块但保持API不变

任务:

  • 租户后台添加广告管理菜单项
  • 租户后台添加路由配置(指向新的管理员API)
  • 租户后台API初始化
  • Admin后台删除广告管理菜单项
  • Admin后台删除广告路由配置
  • Server包替换模块导入: @d8d/advertisements-module-mt@d8d/unified-advertisements-module
  • 保持路由不变: /api/v1/advertisements 路由保持,只是数据源切换
  • 数据源注册新实体(UnifiedAdvertisement, UnifiedAdvertisementType
  • E2E测试验证(重点:验证小程序端API兼容性)

完成日期: 2026-01-03 测试结果: 集成测试 17/17 通过,E2E测试 50/50 通过,5个跳过 相关文件: docs/stories/010.006.story.md

测试覆盖:

  • 管理员广告API权限控制: 4/4 通过
  • 管理员广告类型API权限控制: 3/3 通过
  • 用户端广告API访问控制: 3/3 通过
  • 统一广告数据隔离验证: 1/1 通过
  • API路径兼容性验证: 2/2 通过
  • 管理员操作权限验证: 4/4 通过
  • E2E测试: 50个测试通过(API兼容性验证)
  • 测试文档: 创建E2E测试规范文档,更新测试策略文档

实施内容:

  1. 租户后台集成 (web/src/client/tenant/):

    • 添加广告管理和广告类型管理路由
    • 添加菜单项(Megaphone图标)
    • 初始化API客户端(指向管理员API)
  2. Admin后台移除 (web/src/client/admin/):

    • 移除广告管理和广告类型管理路由
    • 移除菜单项
  3. Server包模块替换 (packages/server/):

    • 替换导入:@d8d/advertisements-module-mt@d8d/unified-advertisements-module
    • 替换实体:Advertisement, AdvertisementTypeUnifiedAdvertisement, UnifiedAdvertisementType
    • 替换路由并添加管理员路由
    • 注册新实体到数据源
    • 添加管理员API路由:/api/v1/admin/unified-advertisements/api/v1/admin/unified-advertisement-types
  4. 测试验证:

    • 创建E2E测试验证API兼容性:web/tests/e2e/unified-advertisement-api.spec.ts
    • 创建集成测试验证管理员权限:packages/server/tests/integration/unified-advertisement-auth.integration.test.ts

关键注意事项:

  • API路由路径 /api/v1/advertisements 保持不变
  • Schema响应结构保持与原模块一致
  • 小程序端无需感知后端模块切换

Story 7: 租户后台UI交互E2E测试 🔄 进行中

标题: 租户后台统一广告管理UI交互E2E测试

描述: 故事010.006的E2E测试只验证了API兼容性(使用request对象),没有覆盖真正的UI交互测试。本故事补充使用Playwright的page对象进行浏览器页面操作,验证租户后台的完整UI交互流程。

背景说明:

  • 当前E2E测试 (unified-advertisement-api.spec.ts) 使用 request 对象直接调用API
  • 这实际上是API集成测试,而非真正的端到端UI测试
  • 需要补充使用 page 对象的UI交互测试,验证:
    • 登录租户后台
    • 导航到广告管理页面
    • 创建、编辑、删除广告
    • 验证页面元素、表单交互、数据展示

任务:

  • 创建UI交互E2E测试文件:web/tests/e2e/tenant-advertisement-ui.spec.ts
  • 测试登录流程:超级管理员登录租户后台
  • 测试导航:验证广告管理菜单项可点击,页面正确跳转
  • 测试广告列表:验证广告列表正确显示,包含正确数据
  • 测试创建广告:打开创建表单,填写字段,提交,验证创建成功
  • 测试编辑广告:点击编辑按钮,修改数据,保存,验证更新成功
  • 测试删除广告:点击删除按钮,确认删除,验证数据删除
  • 测试广告类型管理:验证类型列表、创建、编辑、删除
  • 测试分页功能:验证翻页功能正常工作
  • 测试搜索功能:验证按标题/代码搜索功能正常
  • 测试表单验证:验证必填字段、格式验证、错误提示
  • 测试图片上传:验证图片选择器集成正常工作
  • 测试响应式布局:验证页面在不同屏幕尺寸下正常显示
  • 更新E2E测试规范文档,添加UI交互测试示例

相关文件: docs/stories/010.007.story.md

测试覆盖:

  • 登录和导航流程
  • 广告管理CRUD操作
  • 广告类型管理CRUD操作
  • 表单验证和错误处理
  • 分页和搜索功能
  • 图片选择器集成
  • 响应式布局验证

Story 8: 小程序端广告展示E2E测试 🔄 进行中

标题: 小程序端广告展示功能E2E测试验证

描述: 史诗010的核心目标之一是"小程序端无需感知后端模块切换"。本故事通过E2E测试验证小程序端能够正常展示统一广告数据,确保后端模块切换对小程序端完全透明。

背景说明:

  • 后端模块从 advertisements-module-mt 切换到 unified-advertisements-module
  • API路径和响应结构保持100%兼容
  • 小程序端代码无需任何修改
  • 需要验证小程序端能正常获取和展示广告数据

关键验证点:

  • 小程序启动后能正确调用 /api/v1/advertisements 获取广告
  • 广告图片正确显示
  • 点击广告能正确跳转(webview/小程序页面)
  • 不同位置(position)的广告正确展示
  • 广告状态控制(启用/禁用)生效
  • 多租户用户看到的是相同的统一广告数据

任务:

  • 创建小程序E2E测试文件:mini/tests/e2e/advertisement-display.spec.ts
  • 测试广告列表获取:验证小程序能成功获取广告列表
  • 测试广告图片显示:验证广告图片URL正确,图片能正常加载
  • 测试广告点击跳转:验证webview类型和小程序页面类型跳转正常
  • 测试位置过滤:验证不同位置(home/category等)的广告正确显示
  • 测试状态控制:验证禁用的广告不显示
  • 测试多租户统一数据:验证不同租户用户看到相同广告数据
  • 测试广告类型获取:验证 /api/v1/advertisement-types 接口正常
  • 测试网络异常处理:验证API调用失败时的错误处理
  • 测试数据缓存:验证广告数据的缓存机制正常工作
  • 更新测试策略文档,添加小程序E2E测试规范

相关文件: docs/stories/010.008.story.md

测试覆盖:

  • API调用和数据获取
  • 广告图片显示和加载
  • 广告点击和跳转
  • 位置和状态过滤
  • 多租户数据统一性
  • 错误处理和缓存

Story 9: 创建统一文件后端模块 ✅ 已完成

标题: 创建统一文件后端模块 (unified-file-module)

描述: 当前统一广告模块使用多租户的文件模块(file-module-mt),这与统一广告的无租户隔离设计不一致。本故事从单租户的文件模块复制创建统一版本。

背景说明:

  • 当前问题: 统一广告模块 (unified-advertisements-module) 使用 @d8d/core-module-mt/file-module-mtFileMt 实体(有tenant_id)
  • 不一致性: 统一广告本身是无租户隔离的,但关联的文件却是多租户隔离的
  • 解决方案: 从 file-module 复制创建 unified-file-module(无tenant_id)

任务:

  • 创建 packages/unified-file-module 包(从 file-module 复制并改造)
  • 定义Entity(无tenant_id字段)
  • 实现Service层和文件上传逻辑(MinIO)
  • 实现管理员路由(使用 tenantAuthMiddleware
  • 编写完整的单元测试和集成测试

完成日期: 2026-01-04 相关文件: docs/stories/010.009.story.md

测试成果:

  • 单元测试: 14/14 通过
  • 集成测试: 8/8 通过
  • 总计: 22 个测试全部通过
  • 测试覆盖率: 59.47% (核心业务代码 >70%)

新增模块:

packages/unified-file-module/
├── src/
│   ├── entities/unified-file.entity.ts
│   ├── services/unified-file.service.ts
│   ├── schemas/unified-file.schema.ts
│   └── routes/ (所有路由使用 tenantAuthMiddleware)
└── tests/
    ├── unit/ (14个测试)
    └── integration/ (8个测试)

Story 10: 创建统一文件管理UI包 ✅ 已完成

标题: 创建统一文件管理UI包 (unified-file-management-ui)

描述: 当前统一广告管理UI使用多租户的文件管理UI(file-management-ui-mt),这与统一广告的无租户隔离设计不一致。本故事从单租户的文件管理UI复制创建统一版本。

背景说明:

  • 当前问题: 统一广告管理UI (unified-advertisement-management-ui) 使用 @d8d/file-management-ui-mtFileSelector 组件(多租户)
  • 不一致性: 统一广告管理UI本身是无租户隔离的,但使用的文件选择器却是多租户版本
  • 解决方案: 从 file-management-ui 复制创建 unified-file-management-ui(API指向统一文件模块)

前置条件: 故事010.009已完成

任务:

  • 创建 packages/unified-file-management-ui 包(从 file-management-ui 复制并改造)
  • 实现文件管理组件(列表、上传、删除)
  • 实现文件选择器组件(供其他UI包使用)
  • API客户端指向统一文件模块端点
  • 编写完整的组件测试和集成测试
  • 类型定义规范: 使用RPC推断类型而非从schema获取类型

完成日期: 2026-01-04 相关文件: docs/stories/010.010.story.md

测试成果:

  • 单元测试: 9/9 通过 (useFileManagement hook)
  • 组件测试: 21/21 通过 (FileManagement + FileSelector)
  • 总计: 30 个测试全部通过

新增包:

packages/unified-file-management-ui/
├── src/
│   ├── api/unifiedFileClient.ts (RPC客户端)
│   ├── components/FileManagement.tsx, FileSelector.tsx
│   ├── hooks/useFileManagement.ts, useFileSelector.ts
│   ├── types/file.ts (使用RPC推断类型)
│   └── utils/minio.ts
└── tests/
    ├── components/ (13个测试)
    └── hooks/ (9个测试)

Story 11: 集成统一文件模块到统一广告和租户后台 ✅ 已完成

标题: 集成统一文件模块到统一广告和租户后台

描述: 将统一文件模块集成到统一广告模块、统一广告管理UI、Server包和租户后台。

前置条件:

  • 故事010.009已完成:统一文件模块已创建
  • 故事010.010已完成:统一文件管理UI已创建

任务:

  • 统一广告模块更新为使用 UnifiedFile 实体(而非 FileMt
  • 统一广告管理UI更新为使用统一文件选择器(而非多租户版本)
  • Server包注册统一文件模块路由和实体
  • 租户后台集成统一文件管理功能
  • E2E测试验证文件上传和选择器功能
  • 回归测试确保统一广告模块功能不受影响

完成日期: 2026-01-04 相关文件: docs/stories/010.011.story.md

测试成果:

  • 统一广告模块: 57/57 测试通过
  • 统一广告管理UI: 51/51 测试通过
  • Server包: 68/69 测试通过(1个失败是现有问题)
  • E2E测试文件已创建(需浏览器环境运行)

实施内容:

  1. 实体迁移: UnifiedAdvertisement.imageFileFileMt 迁移到 UnifiedFile
  2. 依赖更新: 3个包的依赖已更新
  3. 路由注册: Server包已注册 /api/v1/admin/unified-files 路由
  4. 租户后台集成: 文件管理菜单和路由已添加
  5. 类型系统扩展: AuthContext 添加 superAdminId 字段

Story 12: 统一广告模块响应格式规范化 🔄 进行中

标题: 统一广告模块响应格式规范化

描述: 修复统一广告模块的API响应格式,使其符合项目 shared-crud 标准格式规范。

背景说明:

  • 统一广告模块实施时使用了非标准响应格式(嵌套在 code/message/data 结构中)
  • 项目标准格式(shared-crud/generic-crud.routes.ts)要求列表响应为 { data: [...], pagination: {...} }
  • 当前格式与UI包的数据处理模式不一致,影响代码可维护性

任务:

  • 修改管理员广告路由响应格式(admin/unified-advertisements.admin.routes.ts
  • 修改管理员广告类型路由响应格式(admin/unified-advertisement-types.admin.routes.ts
  • 修改用户端广告路由响应格式(unified-advertisements.crud.routes.ts
  • 修改用户端广告类型路由响应格式(unified-advertisement-types.crud.routes.ts
  • 适配统一广告管理UI(hooks和测试)
  • 更新后端集成测试
  • 更新Server包集成测试

响应格式变更:

// 列表响应: 当前 → 标准
{ code: 200, message: 'success', data: { list: [...], total, page, pageSize } }
→
{ data: [...], pagination: { total, current, pageSize } }

// 单项响应: 当前 → 标准
{ code: 200, message: 'success', data: { id, ... } }
→
{ id, ... }  // 直接返回资源对象

// 删除响应: 当前 → 标准
{ code: 200, message: '...' }
→
204 No Content  // 空响应

相关文件: docs/stories/010.012.story.md

兼容性要求

  • 现有广告API端点保持向后兼容(或提供适配层)
  • 数据库schema变更不影响现有表
  • UI变更遵循现有租户后台模式
  • 性能影响最小化

风险缓解

主要风险

  1. 权限控制错误: 管理员接口可能被普通租户访问
  2. 现有广告数据迁移: 如果需要迁移历史数据,可能有数据丢失风险
  3. 响应数据结构不一致: 统一模块的Schema可能与原模块有差异

缓解措施

  1. API兼容性:
    • 关键: 用户端路由 /api/v1/advertisements 保持不变
    • Schema定义保持与原 advertisements-module-mt 一致
    • 只在server包中切换模块引用,API契约100%兼容
    • 小程序端无需任何改动,无需重新发布
  2. 数据迁移:
    • 评估现有广告数据价值
    • 如需迁移,创建迁移脚本将精选广告迁移到统一表
  3. 权限控制:
    • 管理员路由严格使用 tenantAuthMiddleware(仅超级管理员ID=1可访问)
    • 用户路由使用 authMiddleware 进行多租户认证(认证通过但返回统一数据)

回滚计划

  • 保留 advertisements-module-mt 包不动
  • 如需回滚,恢复 web/src/client/admin/ 中的菜单和路由
  • 恢复 packages/server/src/index.ts 中的模块引用

详细设计

API端点设计

管理员接口 (超级管理员专用 - 新增)

# 使用 tenantAuthMiddleware (仅超级管理员ID=1可访问)
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   # 删除广告类型

用户展示接口 (保持不变 - 小程序端使用)

# 关键设计: 路由、参数、响应结构完全不变,仅切换后端模块
# 从: advertisements-module-mt  (多租户,tenant_id隔离)
# 到:   unified-advertisements-module (统一,无tenant_id)
# 使用 authMiddleware (多租户认证,但返回统一的广告数据)

GET    /api/v1/advertisements          # 获取有效广告列表(不变)
GET    /api/v1/advertisements/:id      # 获取单个广告详情(不变)
GET    /api/v1/advertisement-types     # 获取广告类型列表(不变)

重要: 小程序端无需任何改动,API契约100%兼容。只是后端数据源从多租户切换到统一模块。

数据库Schema

-- 统一广告表(无tenant_id字段,参考advertisements-module-mt的ad_mt表结构)
CREATE TABLE unified_advertisement (
  id SERIAL PRIMARY KEY,
  title VARCHAR(30) COMMENT '标题',
  type_id INT UNSIGNED NULL COMMENT '广告类型ID',
  code VARCHAR(20) COMMENT '调用别名',
  url VARCHAR(255) COMMENT '跳转URL',
  image_file_id INT UNSIGNED NULL COMMENT '图片文件ID(关联file_module)',
  sort INT DEFAULT 0 COMMENT '排序',
  status INT UNSIGNED DEFAULT 0 COMMENT '状态',
  action_type INT DEFAULT 1 COMMENT '跳转类型: 0=不跳转, 1=webview, 2=小程序页面',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  created_by INT UNSIGNED NULL COMMENT '创建用户ID',
  updated_by INT UNSIGNED NULL COMMENT '更新用户ID',
  FOREIGN KEY (type_id) REFERENCES unified_advertisement_type(id),
  FOREIGN KEY (image_file_id) REFERENCES file_mt(id),
  INDEX idx_type_id (type_id),
  INDEX idx_image_file_id (image_file_id),
  INDEX idx_status (status),
  INDEX idx_sort (sort)
) COMMENT='统一广告表';

CREATE TABLE unified_advertisement_type (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL COMMENT '类型名称',
  code VARCHAR(50) NOT NULL UNIQUE COMMENT '类型代码',
  description TEXT COMMENT '描述',
  status INT DEFAULT 1 COMMENT '状态',
  sort_order INT DEFAULT 0 COMMENT '排序',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_code (code)
) COMMENT='统一广告类型表';

关键设计:

  • image_file_id: 关联 file_module 的文件ID(不是直接存储URL)
  • type_id: 关联广告类型表
  • tenant_id 字段(与原多租户模块的主要区别)
  • 表结构参考 packages/advertisements-module-mt/src/entities/advertisement.entity.ts

Entity定义(TypeORM)

// src/entities/unified-advertisement.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
import { FileMt } from '@d8d/core-module-mt/file-module-mt';  // 规范: 从核心包路径引用
import { UnifiedAdvertisementType } from './unified-advertisement-type.entity';

@Entity('unified_advertisement')
export class UnifiedAdvertisement {
  @PrimaryGeneratedColumn({ unsigned: true })
  id!: number;

  // 注意: 无 tenantId 字段(与原多租户模块的主要区别)

  @Column({
    name: 'title',
    type: 'varchar',
    length: 30,
    nullable: true,
    comment: '标题'
  })
  title!: string | null;

  @Column({
    name: 'type_id',
    type: 'int',
    nullable: true,
    unsigned: true,
    comment: '广告类型'
  })
  typeId!: number | null;

  @Column({
    name: 'code',
    type: 'varchar',
    length: 20,
    nullable: true,
    comment: '调用别名'
  })
  code!: string | null;

  @Column({
    name: 'url',
    type: 'varchar',
    length: 255,
    nullable: true,
    comment: 'url'
  })
  url!: string | null;

  // 文件模块关联(关键设计)
  @Column({
    name: 'image_file_id',
    type: 'int',
    unsigned: true,
    nullable: true,
    comment: '图片文件ID'
  })
  imageFileId!: number | null;

  @ManyToOne(() => FileMt, { nullable: true })
  @JoinColumn({
    name: 'image_file_id',
    referencedColumnName: 'id'
  })
  imageFile!: FileMt | null;

  @ManyToOne(() => UnifiedAdvertisementType, { nullable: true })
  @JoinColumn({
    name: 'type_id',
    referencedColumnName: 'id'
  })
  advertisementType!: UnifiedAdvertisementType | null;

  @Column({
    name: 'sort',
    type: 'int',
    default: 0,
    comment: '排序'
  })
  sort!: number;

  @CreateDateColumn({
    name: 'created_at',
    type: 'timestamp',
    comment: '创建时间'
  })
  createdAt!: Date;

  @UpdateDateColumn({
    name: 'updated_at',
    type: 'timestamp',
    comment: '更新时间'
  })
  updatedAt!: Date;

  @Column({
    name: 'created_by',
    type: 'int',
    unsigned: true,
    nullable: true,
    comment: '创建用户ID'
  })
  createdBy!: number | null;

  @Column({
    name: 'updated_by',
    type: 'int',
    unsigned: true,
    nullable: true,
    comment: '更新用户ID'
  })
  updatedBy!: number | null;

  @Column({
    name: 'status',
    type: 'int',
    unsigned: true,
    default: 0,
    comment: '状态'
  })
  status!: number;

  @Column({
    name: 'action_type',
    type: 'int',
    default: 1,
    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
  })
  actionType!: number;
}

路由实现参考

统一广告模块路由

// src/routes/admin/advertisements.ts - 管理员路由(新增)
import { createCrudRoutes } from '@d8d/shared-crud';
import { tenantAuthMiddleware } from '@d8d/tenant-module-mt';
import { UnifiedAdvertisement } from '../entities/unified-advertisement.entity';

export const adminUnifiedAdRoutes = createCrudRoutes({
  entity: UnifiedAdvertisement,
  createSchema: CreateUnifiedAdvertisementDto,
  updateSchema: UpdateUnifiedAdvertisementDto,
  getSchema: UnifiedAdvertisementSchema,
  listSchema: UnifiedAdvertisementSchema,
  searchFields: ['title', 'code'],
  relations: ['advertisementType', 'imageFile'],  // 关键: 包含imageFile关联
  middleware: [tenantAuthMiddleware], // 超级管理员认证
  userTracking: {
    createdByField: 'createdBy',
    updatedByField: 'updatedBy'
  },
  dataPermission: {
    enabled: false // 不启用数据权限,所有超级管理员共享
  }
});

// src/routes/advertisements.ts - 用户路由(与原模块保持一致)
import { createCrudRoutes } from '@d8d/shared-crud';
import { authMiddleware } from '@d8d/auth-module-mt';
import { UnifiedAdvertisement } from '../entities/unified-advertisement.entity';

// 关键: 路由结构与原advertisements-module-mt完全一致
// 使用authMiddleware进行多租户认证,但返回统一数据(无tenant_id过滤)
export const advertisementRoutes = createCrudRoutes({
  entity: UnifiedAdvertisement,
  createSchema: CreateAdvertisementDto,
  updateSchema: UpdateAdvertisementDto,
  getSchema: AdvertisementSchema,
  listSchema: AdvertisementSchema,
  searchFields: ['title', 'code'],
  relations: ['advertisementType', 'imageFile'],  // 关键: 包含imageFile关联
  middleware: [authMiddleware],
  // 注意: 不使用tenantOptions,返回统一数据给所有租户
  userTracking: {
    createdByField: 'createdBy',
    updatedByField: 'updatedBy'
  },
  dataPermission: {
    enabled: false // 不启用数据权限控制
  }
});

Server包模块替换

// packages/server/src/index.ts - 变更对比

// ===== 旧代码 (删除) =====
import { Advertisement, AdvertisementType } from '@d8d/advertisements-module-mt';
import { advertisementRoutes, advertisementTypeRoutes } from '@d8d/advertisements-module-mt';

// ===== 新代码 (使用) =====
import { UnifiedAdvertisement, UnifiedAdvertisementType } from '@d8d/unified-advertisements-module';
import { advertisementRoutes, advertisementTypeRoutes } from '@d8d/unified-advertisements-module';
import { adminUnifiedAdRoutes } from '@d8d/unified-advertisements-module';

// ===== 数据源注册 =====
initializeDataSource([
  // ...
  // 旧: Advertisement, AdvertisementType,
  新: UnifiedAdvertisement, UnifiedAdvertisementType,
]);

// ===== 路由注册 - 用户端保持不变 =====
export const advertisementApiRoutes = api.route('/api/v1/advertisements', advertisementRoutes);
export const advertisementTypeApiRoutes = api.route('/api/v1/advertisement-types', advertisementTypeRoutes);

// ===== 路由注册 - 管理员端(新增) =====
export const adminUnifiedAdApiRoutes = api.route('/api/v1/admin/unified-advertisements', adminUnifiedAdRoutes);

验收标准

完成定义 (Definition of Done)

  • 所有故事完成且验收标准满足(Story 7、8 待完成;Story 9、10、11 已完成)
  • 现有功能通过测试验证
  • 集成点正常工作
  • 文档适当更新
  • 现有功能无回归

功能验收

  1. 租户后台(超级管理员)可以管理广告(创建、编辑、删除、查看)
  2. 所有租户用户可以读取到统一的广告数据
  3. Admin后台不再显示广告管理入口
  4. API端点正常工作且返回正确数据
  5. 权限控制正确(只有超级管理员可管理)
  6. 租户后台UI交互E2E测试覆盖完整流程(Story 7)
  7. 小程序端广告展示E2E测试验证通过(Story 8)
  8. 统一文件模块创建完成(Story 9)
  9. 统一文件管理UI创建完成(Story 10)
  10. 统一文件模块集成到统一广告和租户后台(Story 11)

技术验收

  1. 所有单元测试通过
  2. 集成测试通过
  3. 租户后台UI交互E2E测试通过(Story 7)
  4. 小程序端广告展示E2E测试通过(Story 8)
  5. 统一文件模块测试通过(Story 9)
  6. 统一文件管理UI测试通过(Story 10)
  7. 集成和回归测试通过(Story 11)
  8. 代码符合项目编码规范
  9. 无TypeScript类型错误
  10. ESLint检查通过

参考文档


Story Manager Handoff:

"请为这个brownfield史诗开发详细的用户故事。关键考虑事项:

  • 这是对现有运行系统的增强,技术栈: Hono + TypeORM + React + Vite
  • 集成点:
    • Server包: packages/server/src/index.ts
    • 租户后台: web/src/client/tenant/
    • Admin后台: web/src/client/admin/
  • 需要遵循的现有模式:
    • 管理员接口使用 tenantAuthMiddleware (参考 packages/tenant-module-mt/)
    • 用户接口使用 authMiddleware (参考 packages/advertisements-module-mt/)
  • 关键兼容性要求:
    • 保持现有API端点兼容
    • 不影响小程序端广告读取
    • 权限控制严格区分管理员和用户
  • 每个故事必须包含验证现有功能完整性的测试

史诗应在保持系统完整性的同时交付统一广告管理功能。"