| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 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 字段隔离packages/server/src/index.ts 注册广告路由web/src/client/admin/ 使用 @d8d/advertisement-management-ui-mt/api/v1/advertisements 读取广告添加/修改内容:
packages/unified-advertisements-module - 统一广告模块(无tenant_id隔离)packages/unified-advertisement-management-ui - 统一广告管理UI包web/src/client/tenant/)集成方式:
tenantAuthMiddleware(超级管理员专用,参考租户模块实现)authMiddleware(多租户认证,但返回统一的广告数据)tenant_id 字段的表结构成功标准:
标题: 创建统一广告后端模块 (unified-advertisements-module)
描述: 复制单租户广告模块并改造,移除tenant_id字段,区分管理员和用户接口
任务:
完成日期: 2026-01-02(初始),2026-01-03(任务11)
测试覆盖: 57个测试全部通过(23个单元测试 + 34个集成测试)
相关文件: docs/stories/010.001.story.md
标题: 创建统一广告管理UI包 (unified-advertisement-management-ui)
描述: 复制单租户广告管理UI并改造,API端点指向统一模块
任务:
完成日期: 2026-01-03
测试覆盖: 13个集成测试全部通过
相关文件: docs/stories/010.002.story.md
标题: 修复统一广告模块路由路径规范问题
描述: 故事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/v1/admin/unified-advertisements → //api/v1/admin/unified-advertisements/:id → /:id/api/v1/admin/unified-advertisement-types → //api/v1/admin/unified-advertisement-types/:id → /:id/api/v1/advertisement-types → /adminClient.$get() 而非 adminClient['/path'].$get())完成日期: 2026-01-03
相关文件: docs/stories/010.003.story.md
标题: 修复统一广告模块路由参数类型规范问题
描述: 统一广告模块的路由定义中缺少 params schema 定义,导致 RPC 客户端推断出的 :id 参数类型为 string,而不是 number。需要在路由 schema 中添加 params 定义,使用 z.coerce.number() 进行类型转换。
问题说明:
request.params,导致 RPC 客户端推断 :id 为 string 类型z.coerce.number<number>() 定义 params,自动转换 string 到 numbercreateCrudRoutes 的开发规范,所有路径参数都应明确定义类型任务:
unified-advertisements.admin.routes.ts 的 params 定义(getRoute, updateRoute, deleteRoute)unified-advertisement-types.admin.routes.ts 的 params 定义修复内容:
request.params 定义,使用 z.coerce.number<number>()request.params 定义request.params 定义String(id) 类型转换c.req.valid('param') 替代 parseInt(c.req.param('id'))完成日期: 2026-01-03
测试结果: 57/57 测试通过
相关文件: docs/stories/010.004.story.md
标题: 补充统一广告管理UI包测试覆盖度
描述: 为统一广告管理UI包补充缺失的测试场景,提升测试覆盖率到70%以上,确保代码质量和稳定性。
背景说明:
任务:
完成日期: 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 - 图片选择器测试标题: 集成到租户后台、移除admin后台广告管理、Server切换模块
描述: 将统一广告管理UI集成到租户后台,从admin后台移除广告管理,关键:Server包切换模块但保持API不变
任务:
@d8d/advertisements-module-mt → @d8d/unified-advertisements-module/api/v1/advertisements 路由保持,只是数据源切换UnifiedAdvertisement, UnifiedAdvertisementType)完成日期: 2026-01-03
测试结果: 集成测试 17/17 通过,E2E测试 50/50 通过,5个跳过
相关文件: docs/stories/010.006.story.md
测试覆盖:
实施内容:
租户后台集成 (web/src/client/tenant/):
Admin后台移除 (web/src/client/admin/):
Server包模块替换 (packages/server/):
@d8d/advertisements-module-mt → @d8d/unified-advertisements-moduleAdvertisement, AdvertisementType → UnifiedAdvertisement, UnifiedAdvertisementType/api/v1/admin/unified-advertisements 和 /api/v1/admin/unified-advertisement-types测试验证:
web/tests/e2e/unified-advertisement-api.spec.tspackages/server/tests/integration/unified-advertisement-auth.integration.test.ts关键注意事项:
/api/v1/advertisements 保持不变标题: 租户后台统一广告管理UI交互E2E测试
描述: 故事010.006的E2E测试只验证了API兼容性(使用request对象),没有覆盖真正的UI交互测试。本故事补充使用Playwright的page对象进行浏览器页面操作,验证租户后台的完整UI交互流程。
背景说明:
unified-advertisement-api.spec.ts) 使用 request 对象直接调用APIpage 对象的UI交互测试,验证:
任务:
web/tests/e2e/tenant-advertisement-ui.spec.ts相关文件: docs/stories/010.007.story.md
测试覆盖:
标题: 小程序端广告展示功能E2E测试验证
描述: 史诗010的核心目标之一是"小程序端无需感知后端模块切换"。本故事通过E2E测试验证小程序端能够正常展示统一广告数据,确保后端模块切换对小程序端完全透明。
背景说明:
advertisements-module-mt 切换到 unified-advertisements-module关键验证点:
/api/v1/advertisements 获取广告任务:
mini/tests/e2e/advertisement-display.spec.ts/api/v1/advertisement-types 接口正常相关文件: docs/stories/010.008.story.md
测试覆盖:
标题: 创建统一文件后端模块 (unified-file-module)
描述: 当前统一广告模块使用多租户的文件模块(file-module-mt),这与统一广告的无租户隔离设计不一致。本故事从单租户的文件模块复制创建统一版本。
背景说明:
unified-advertisements-module) 使用 @d8d/core-module-mt/file-module-mt 的 FileMt 实体(有tenant_id)file-module 复制创建 unified-file-module(无tenant_id)任务:
packages/unified-file-module 包(从 file-module 复制并改造)tenantAuthMiddleware)完成日期: 2026-01-04
相关文件: docs/stories/010.009.story.md
测试成果:
新增模块:
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个测试)
标题: 创建统一文件管理UI包 (unified-file-management-ui)
描述: 当前统一广告管理UI使用多租户的文件管理UI(file-management-ui-mt),这与统一广告的无租户隔离设计不一致。本故事从单租户的文件管理UI复制创建统一版本。
背景说明:
unified-advertisement-management-ui) 使用 @d8d/file-management-ui-mt 的 FileSelector 组件(多租户)file-management-ui 复制创建 unified-file-management-ui(API指向统一文件模块)前置条件: 故事010.009已完成
任务:
packages/unified-file-management-ui 包(从 file-management-ui 复制并改造)完成日期: 2026-01-04
相关文件: docs/stories/010.010.story.md
测试成果:
新增包:
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个测试)
标题: 集成统一文件模块到统一广告和租户后台
描述: 将统一文件模块集成到统一广告模块、统一广告管理UI、Server包和租户后台。
前置条件:
任务:
UnifiedFile 实体(而非 FileMt)完成日期: 2026-01-04
相关文件: docs/stories/010.011.story.md
测试成果:
实施内容:
UnifiedAdvertisement.imageFile 从 FileMt 迁移到 UnifiedFile/api/v1/admin/unified-files 路由AuthContext 添加 superAdminId 字段标题: 统一广告模块响应格式规范化
描述: 修复统一广告模块的API响应格式,使其符合项目 shared-crud 标准格式规范。
背景说明:
code/message/data 结构中)shared-crud/generic-crud.routes.ts)要求列表响应为 { data: [...], pagination: {...} }任务:
admin/unified-advertisements.admin.routes.ts)admin/unified-advertisement-types.admin.routes.ts)unified-advertisements.crud.routes.ts)unified-advertisement-types.crud.routes.ts)响应格式变更:
// 列表响应: 当前 → 标准
{ 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/v1/advertisements 保持不变advertisements-module-mt 一致tenantAuthMiddleware(仅超级管理员ID=1可访问)authMiddleware 进行多租户认证(认证通过但返回统一数据)advertisements-module-mt 包不动web/src/client/admin/ 中的菜单和路由packages/server/src/index.ts 中的模块引用# 使用 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%兼容。只是后端数据源从多租户切换到统一模块。
-- 统一广告表(无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// 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 // 不启用数据权限控制
}
});
// 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);
packages/tenant-module-mt/packages/advertisements-module/Story Manager Handoff:
"请为这个brownfield史诗开发详细的用户故事。关键考虑事项:
packages/server/src/index.tsweb/src/client/tenant/web/src/client/admin/tenantAuthMiddleware (参考 packages/tenant-module-mt/)authMiddleware (参考 packages/advertisements-module-mt/)史诗应在保持系统完整性的同时交付统一广告管理功能。"