Parcourir la source

📝 docs(architecture): 新增史诗010和核心包引用规范

- 新增史诗010文档:统一广告管理系统
  - 从多租户广告管理迁移到统一广告管理
  - 超管在租户后台统一管理广告,所有租户共享
  - 保持mini小程序API兼容性

- 更新后端模块开发规范
  - 新增9.3节:核心包引用规范
  - 规定新模块必须从 @d8d/core-module-mt/xxx 引用
  - 添加Entity和路由引用示例
  - 更新检查清单

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname il y a 2 semaines
Parent
commit
1b2b272a59

+ 45 - 0
docs/architecture/backend-module-package-standards.md

@@ -725,6 +725,50 @@ app.delete('/:id', async (c) => {
 }
 ```
 
+### 9.3 核心包引用规范
+
+**重要**: 新创建的模块(在 `packages/` 或 `allin-packages/` 下)必须从核心包路径引用基础模块,而不是直接从桥接包引用。
+
+#### 引用规范对照表
+
+| 模块类型 | ❌ 错误引用 | ✅ 正确引用 |
+|---------|-----------|-----------|
+| 文件模块(多租户) | `@d8d/file-module-mt` | `@d8d/core-module-mt/file-module-mt` |
+| 用户模块(多租户) | `@d8d/user-module-mt` | `@d8d/core-module-mt/user-module-mt` |
+| 认证模块(多租户) | `@d8d/auth-module-mt` | `@d8d/core-module-mt/auth-module-mt` |
+| 系统配置模块 | `@d8d/system-config-module-mt` | `@d8d/core-module-mt/system-config-module-mt` |
+
+#### Entity引用示例
+
+```typescript
+// ❌ 错误:直接从桥接包引用
+import { FileMt } from '@d8d/file-module-mt';
+import { UserEntityMt } from '@d8d/user-module-mt';
+
+// ✅ 正确:从核心包路径引用
+import { FileMt } from '@d8d/core-module-mt/file-module-mt';
+import { UserEntityMt } from '@d8d/core-module-mt/user-module-mt';
+```
+
+#### 路由引用示例
+
+```typescript
+// ❌ 错误
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { authMiddleware } from '@d8d/auth-module-mt/middleware';
+
+// ✅ 正确
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt';
+import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt/middleware';
+```
+
+#### 为什么需要这个规范?
+
+1. **统一管理**: 核心包 (`core-module-mt`) 聚合了所有基础模块,提供统一的引用入口
+2. **避免循环依赖**: 通过核心包路径引用,可以更好地管理模块间的依赖关系
+3. **保持一致性**: 所有新模块使用相同的引用模式,代码更易维护
+4. **未来兼容性**: 当模块重构时,只需更新核心包的导出,不需要修改所有引用处
+
 ## 10. 测试规范
 
 详细的测试规范请参考 [后端模块包测试规范](./backend-module-testing-standards.md)。
@@ -845,6 +889,7 @@ override async create(data: Partial<Channel>, userId?: string | number): Promise
 
 - [ ] 包名符合规范:`@d8d/allin-{name}-module` 或 `@d8d/core-module`
 - [ ] 目录结构完整:entities, services, routes, schemas, tests
+- [ ] **核心模块引用使用核心包路径**: 使用 `@d8d/core-module-mt/xxx` 而非 `@d8d/xxx-module-mt`
 - [ ] Entity包含完整列定义:type, comment, nullable等
 - [ ] Service使用 `override` 关键字
 - [ ] 软删除实现:使用 `status` 字段

+ 475 - 0
docs/prd/epic-010-unified-ad-management.md

@@ -0,0 +1,475 @@
+# 史诗 010: 统一广告管理系统 - Brownfield Enhancement
+
+## 版本信息
+
+| 版本 | 日期 | 描述 | 作者 |
+|------|------|------|------|
+| 1.0 | 2026-01-02 | 初始版本 | James (Claude Code) |
+
+## 史诗目标
+
+将多租户广告管理改造为统一广告管理系统,由超级管理员在租户管理后台统一配置广告,所有租户共享相同的广告数据展示。同时保持小程序端广告读取逻辑不受影响。
+
+## 史诗描述
+
+### 现有系统上下文
+
+- **当前相关功能**: 系统使用 `@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)
+- [ ] 编写单元测试和集成测试
+
+### Story 2: 创建统一广告管理UI
+
+**标题**: 创建统一广告管理UI包 (unified-advertisement-management-ui)
+
+**描述**: 复制单租户广告管理UI并改造,API端点指向统一模块
+
+**任务**:
+- [ ] 创建UI包结构
+- [ ] 实现广告管理组件(列表、创建、编辑、删除)
+- [ ] 实现广告类型管理组件
+- [ ] 创建API客户端(指向统一模块端点)
+- [ ] 编写组件测试
+
+### Story 3: 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兼容性)
+
+**关键注意事项**:
+- API路由路径 `/api/v1/advertisements` 保持不变
+- Schema响应结构保持与原模块一致
+- 小程序端无需感知后端模块切换
+
+## 兼容性要求
+
+- [x] 现有广告API端点保持向后兼容(或提供适配层)
+- [x] 数据库schema变更不影响现有表
+- [x] UI变更遵循现有租户后台模式
+- [x] 性能影响最小化
+
+## 风险缓解
+
+### 主要风险
+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
+
+```sql
+-- 统一广告表(无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)
+
+```typescript
+// 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;
+}
+```
+
+### 路由实现参考
+
+#### 统一广告模块路由
+
+```typescript
+// 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包模块替换
+
+```typescript
+// 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)
+- [ ] 所有故事完成且验收标准满足
+- [ ] 现有功能通过测试验证
+- [ ] 集成点正常工作
+- [ ] 文档适当更新
+- [ ] 现有功能无回归
+
+### 功能验收
+1. [ ] 租户后台(超级管理员)可以管理广告(创建、编辑、删除、查看)
+2. [ ] 所有租户用户可以读取到统一的广告数据
+3. [ ] Admin后台不再显示广告管理入口
+4. [ ] API端点正常工作且返回正确数据
+5. [ ] 权限控制正确(只有超级管理员可管理)
+
+### 技术验收
+1. [ ] 所有单元测试通过
+2. [ ] 集成测试通过
+3. [ ] 代码符合项目编码规范
+4. [ ] 无TypeScript类型错误
+5. [ ] ESLint检查通过
+
+## 参考文档
+
+- [后端模块包开发规范](../architecture/backend-module-package-standards.md)
+- [UI包开发规范](../architecture/ui-package-standards.md)
+- [源码树和文件组织](../architecture/source-tree.md)
+- 租户模块实现: `packages/tenant-module-mt/`
+- 单租户广告模块: `packages/advertisements-module/`
+
+---
+
+**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端点兼容
+  - 不影响小程序端广告读取
+  - 权限控制严格区分管理员和用户
+- 每个故事必须包含验证现有功能完整性的测试
+
+史诗应在保持系统完整性的同时交付统一广告管理功能。"