|
|
@@ -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端点兼容
|
|
|
+ - 不影响小程序端广告读取
|
|
|
+ - 权限控制严格区分管理员和用户
|
|
|
+- 每个故事必须包含验证现有功能完整性的测试
|
|
|
+
|
|
|
+史诗应在保持系统完整性的同时交付统一广告管理功能。"
|