# 用户反馈系统 - 架构设计文档

> 架构设计文档 - 定义系统架构、数据模型、核心设计决策
> 
> **版本**: v1.0.3  
> **模块标识**: `platform_feedback`  
> **创建日期**: 2025-12-08  
> **更新日期**: 2026-01-07

---

## 📐 系统架构

### 整体架构图

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Client Layer                                    │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                         Web Application                              │    │
│  │  ┌───────────────────┐  ┌───────────────────┐  ┌─────────────────┐  │    │
│  │  │  反馈悬浮按钮      │  │   我的反馈页面    │  │  反馈管理页面   │  │    │
│  │  │  (FeedbackButton)  │  │  (MyFeedbacks)    │  │ (FeedbackAdmin) │  │    │
│  │  └─────────┬─────────┘  └─────────┬─────────┘  └────────┬────────┘  │    │
│  │            │                      │                      │           │    │
│  │            └──────────────────────┼──────────────────────┘           │    │
│  │                                   │                                  │    │
│  │                                   ▼                                  │    │
│  │                    ┌──────────────────────────┐                      │    │
│  │                    │     Feedback API Client   │                      │    │
│  │                    └──────────────────────────┘                      │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       │ REST API
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              API Layer                                       │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                       NestJS Application                             │    │
│  │  ┌───────────────────────────────────────────────────────────────┐  │    │
│  │  │                     Guards & Interceptors                      │  │    │
│  │  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐ │  │    │
│  │  │  │ JwtAuthGuard │  │PermissionsGuard      │                   │  │    │
│  │  │  └──────────────┘  └──────────────┘  └──────────────────────┘ │  │    │
│  │  └───────────────────────────────────────────────────────────────┘  │    │
│  │                                                                      │    │
│  │  ┌───────────────────────────────────────────────────────────────┐  │    │
│  │  │                     FeedbackController                         │  │    │
│  │  │  ┌────────────────┐  ┌────────────────┐  ┌────────────────┐   │  │    │
│  │  │  │ 用户端接口      │  │ 管理端接口      │  │ 统计接口       │   │  │    │
│  │  │  │ (My Feedbacks) │  │ (Admin CRUD)   │  │ (Stats)        │   │  │    │
│  │  │  └────────────────┘  └────────────────┘  └────────────────┘   │  │    │
│  │  └───────────────────────────────────────────────────────────────┘  │    │
│  │                                                                      │    │
│  │  ┌───────────────────────────────────────────────────────────────┐  │    │
│  │  │                      FeedbackService                           │  │    │
│  │  │  • 反馈创建与查询                                               │  │    │
│  │  │  • 状态流转管理                                                 │  │    │
│  │  │  • 区域权限过滤                                                 │  │    │
│  │  │  • 统计分析                                                     │  │    │
│  │  └───────────────────────────────────────────────────────────────┘  │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────────────┘
                                       │
                                       │ Prisma ORM
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Database Layer                                  │
│  ┌───────────────────────────────────────────────────────────────────────┐  │
│  │                        PostgreSQL Database                             │  │
│  │  ┌─────────────────────────────────────────────────────────────────┐  │  │
│  │  │                  platform_feedback Schema                        │  │  │
│  │  │  ┌───────────────────┐                                           │  │  │
│  │  │  │     feedbacks     │                                           │  │  │
│  │  │  │  (反馈主表)        │                                           │  │  │
│  │  │  └───────────────────┘                                           │  │  │
│  │  └─────────────────────────────────────────────────────────────────┘  │  │
│  │                                                                        │  │
│  │  ┌─────────────────────────────────────────────────────────────────┐  │  │
│  │  │                    platform_iam Schema                           │  │  │
│  │  │  ┌───────────────────┐                                          │  │  │
│  │  │  │       users       │◄─────── 关联用户信息                      │  │  │
│  │  │  └───────────────────┘                                          │  │  │
│  │  └─────────────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
```

### 模块划分

```
┌────────────────────────────────────────────────────────────────────────────┐
│                          platform_feedback Module                           │
├────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      Feedback Module                                 │   │
│  │  (src/modules/feedback/)                                             │   │
│  │                                                                      │   │
│  │  • FeedbackController      - 反馈 API 接口                           │   │
│  │  • FeedbackService         - 反馈业务逻辑                            │   │
│  │  • FeedbackModule          - 模块定义                                │   │
│  │  • dto/                    - 数据传输对象                            │   │
│  │    └── feedback.dto.ts                                               │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└────────────────────────────────────────────────────────────────────────────┘
```

### 模块依赖关系

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Module Dependencies                                │
└─────────────────────────────────────────────────────────────────────────────┘

                    ┌───────────────────────────────┐
                    │        Auth Module            │
                    │  • JWT 验证                   │
                    │  • 用户身份获取                │
                    └───────────────┬───────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │     platform_iam Module       │
                    │  • 用户信息                   │
                    │  • 区域信息 (metadata.region) │
                    │  • 权限验证                   │
                    └───────────────┬───────────────┘
                                    │
                                    ▼
                    ┌───────────────────────────────┐
                    │   platform_feedback Module    │
                    │  • 反馈 CRUD                  │
                    │  • 状态管理                   │
                    │  • 区域过滤                   │
                    └───────────────┘
```

---

## 🗄️ 数据模型

### ER 图

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           platform_feedback Schema                           │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────┐
│           Feedback               │
├──────────────────────────────────┤
│ id (PK)              UUID        │
│ type                 Enum        │◄─── FeedbackType (BUG/FEATURE/...)
│ title                String      │
│ content              Text        │
│ attachments          String[]    │     存储文件 URL 数组
│                                  │
│ pageUrl              String?     │     提交时的页面 URL
│ userAgent            String?     │     浏览器信息
│                                  │
│ status               Enum        │◄─── FeedbackStatus (PENDING/...)
│ priority             Enum?       │◄─── FeedbackPriority (LOW/MEDIUM/...)
│                                  │
│ adminNote            Text?       │     管理员内部备注（用户不可见）
│ adminReply           Text?       │     管理员回复（用户可见）
│ assigneeId (FK)      UUID?       │────► User (指派处理人)
│ resolvedAt           DateTime?   │
│                                  │
│ userId (FK)          UUID        │────► User (提交用户)
│ region               String      │     地域标识，继承自用户 metadata.region
│                                  │
│ createdAt            DateTime    │
│ updatedAt            DateTime    │
│ deletedAt            DateTime?   │     软删除
└──────────────────────────────────┘
            │
            │ N:1
            ▼
┌──────────────────────────────────┐
│            User                  │
│      (platform_iam.users)        │
├──────────────────────────────────┤
│ id (PK)              UUID        │
│ displayName          String      │
│ email                String      │
│ region               String      │
│ ...                              │
└──────────────────────────────────┘
```

### 核心实体定义

#### Feedback（反馈）

```prisma
// backend/prisma/schema/platform_feedback.prisma

model Feedback {
  id              String            @id @default(uuid()) @db.Uuid
  
  // 反馈内容
  type            FeedbackType
  title           String            @db.VarChar(200)
  content         String            @db.Text
  attachments     String[]          @default([])       // 附件 URL 数组
  
  // 上下文信息
  pageUrl         String?           @map("page_url") @db.VarChar(500)
  userAgent       String?           @map("user_agent") @db.VarChar(500)
  
  // 状态管理
  status          FeedbackStatus    @default(PENDING)
  priority        FeedbackPriority?
  
  // 管理员处理
  adminNote       String?           @map("admin_note") @db.Text
  adminReply      String?           @map("admin_reply") @db.Text
  assigneeId      String?           @map("assignee_id") @db.Uuid
  resolvedAt      DateTime?         @map("resolved_at") @db.Timestamptz(3)
  
  // 用户信息
  userId          String            @map("user_id") @db.Uuid
  
  // 多区域支持
  region          String            @default("CN") @db.VarChar(10)    // CN/US/EU/APAC
  
  // 审计
  createdAt       DateTime          @default(now()) @map("created_at") @db.Timestamptz(3)
  updatedAt       DateTime          @updatedAt @map("updated_at") @db.Timestamptz(3)
  deletedAt       DateTime?         @map("deleted_at") @db.Timestamptz(3)
  
  // 关联
  user            User              @relation("FeedbackUser", fields: [userId], references: [id])
  assignee        User?             @relation("FeedbackAssignee", fields: [assigneeId], references: [id])

  @@index([userId])
  @@index([assigneeId])
  @@index([status])
  @@index([type])
  @@index([region])
  @@index([createdAt])
  @@index([region, status])            // 复合索引：区域+状态查询
  @@map("feedbacks")
  @@schema("platform_feedback")
}

enum FeedbackType {
  BUG           // 问题反馈
  FEATURE       // 功能建议
  IMPROVEMENT   // 改进建议
  OTHER         // 其他
  
  @@schema("platform_feedback")
}

enum FeedbackStatus {
  PENDING       // 待处理
  IN_PROGRESS   // 处理中
  RESOLVED      // 已解决
  CLOSED        // 已关闭
  
  @@schema("platform_feedback")
}

enum FeedbackPriority {
  LOW           // 低
  MEDIUM        // 中
  HIGH          // 高
  URGENT        // 紧急
  
  @@schema("platform_feedback")
}
```

#### User 关联扩展

```prisma
// 在 platform_iam.prisma 中添加关联

model User {
  // ... 现有字段 ...
  
  // 反馈关联
  feedbacks          Feedback[]    @relation("FeedbackUser")
  assignedFeedbacks  Feedback[]    @relation("FeedbackAssignee")
}
```

---

## 🎯 核心设计决策

### 决策 1: Region 继承策略

**问题**：反馈的区域标识如何确定？

**方案**：从提交用户继承，不可手动修改

```typescript
// 创建反馈时自动继承用户的 metadata.region
async createFeedback(userId: string, dto: CreateFeedbackDto): Promise<Feedback> {
  const user = await this.prisma.user.findUnique({
    where: { id: userId },
    select: { metadata: true }
  });
  
  return this.prisma.feedback.create({
    data: {
      ...dto,
      userId,
      region: user.metadata?.region || 'CN',  // 默认值
      status: FeedbackStatus.PENDING
    }
  });
}
```

**理由**：
- 反馈自然属于用户所在区域
- 简化管理，避免跨区域数据混乱
- 区域管理员职责明确

### 决策 2: 状态流转控制

**问题**：如何控制反馈状态的合法流转？

**方案**：采用状态机模式，定义合法的状态转换

```typescript
// 状态转换规则
const statusTransitions: Record<FeedbackStatus, FeedbackStatus[]> = {
  [FeedbackStatus.PENDING]: [
    FeedbackStatus.IN_PROGRESS,
    FeedbackStatus.CLOSED
  ],
  [FeedbackStatus.IN_PROGRESS]: [
    FeedbackStatus.RESOLVED,
    FeedbackStatus.CLOSED,
    FeedbackStatus.PENDING  // 可退回
  ],
  [FeedbackStatus.RESOLVED]: [
    FeedbackStatus.CLOSED,
    FeedbackStatus.IN_PROGRESS  // 可重新打开
  ],
  [FeedbackStatus.CLOSED]: [
    FeedbackStatus.PENDING  // 可重新激活
  ]
};

// 状态变更验证
validateStatusTransition(currentStatus: FeedbackStatus, newStatus: FeedbackStatus): void {
  const allowedTransitions = statusTransitions[currentStatus];
  if (!allowedTransitions.includes(newStatus)) {
    throw new BusinessException(
      `无效的状态变更：${currentStatus} -> ${newStatus}`,
      'FEEDBACK_INVALID_STATUS_TRANSITION',
      HttpStatus.BAD_REQUEST
    );
  }
}
```

**状态流转图**：

```
                    ┌─────────────────────────────────────────┐
                    │                                         │
                    ▼                                         │
┌──────────┐     ┌──────────────┐     ┌──────────┐           │
│ PENDING  │────►│ IN_PROGRESS  │────►│ RESOLVED │           │
│  待处理  │     │   处理中     │     │  已解决  │           │
└────┬─────┘     └──────┬───────┘     └────┬─────┘           │
     │                  │                   │                 │
     │                  │                   │                 │
     ▼                  ▼                   ▼                 │
┌────────────────────────────────────────────────┐           │
│                    CLOSED                       │───────────┘
│                    已关闭                       │  (可重新激活)
└────────────────────────────────────────────────┘
```

### 决策 3: 区域权限控制

**问题**：如何实现区域管理员只能管理本区域反馈？

**方案**：Service 层统一过滤

```typescript
// 区域权限过滤
async findAll(adminUserId: string, query: FeedbackQueryDto): Promise<PaginatedResult<Feedback>> {
  const regionFilter = await this.getRegionFilter(adminUserId, query.region);
  
  return this.prisma.feedback.findMany({
    where: {
      deletedAt: null,
      ...regionFilter,
      ...query.filters
    }
  });
}

private async getRegionFilter(
  userId: string,
  requestedRegion?: string,
): Promise<{ region?: any }> {
  // 全局管理员：不做区域限制
  // 区域管理员：仅允许本区域或请求区域
  // 无区域权限：返回空结果
}
```

### 决策 4: 附件存储策略

**问题**：反馈附件（截图）如何存储？

**方案**：使用外部文件存储服务，数据库仅存 URL

```typescript
interface AttachmentStrategy {
  // 上传流程
  upload: '前端直传 → 文件服务 → 返回 URL → 提交反馈时携带 URL';
  
  // 存储格式
  storage: 'String[] - 存储完整的文件访问 URL';
  
  // 服务端校验
  validation: '仅校验 URL 格式与数量';

  // 数量限制
  maxCount: '5 urls per feedback';
}
```

**存储结构**：

```json
{
  "attachments": [
    "https://storage.example.com/feedbacks/uuid1/screenshot1.png",
    "https://storage.example.com/feedbacks/uuid1/screenshot2.png"
  ]
}
```

### 决策 5: 用户可见性控制

**问题**：哪些信息对用户可见，哪些仅管理员可见？

**方案**：字段级别的可见性控制

| 字段 | 用户可见 | 管理员可见 | 说明 |
|------|----------|------------|------|
| `id`, `type`, `title`, `content` | ✅ | ✅ | 基础信息 |
| `status`, `priority` | ✅ | ✅ | 状态信息 |
| `adminReply` | ✅ | ✅ | 管理员回复 |
| `adminNote` | ❌ | ✅ | 内部备注 |
| `assigneeId`, `assignee` | ❌ | ✅ | 处理人信息 |
| `pageUrl`, `userAgent` | ❌ | ✅ | 技术信息 |

```typescript
// DTO 转换示例
toUserDto(feedback: Feedback): FeedbackUserDto {
  return {
    id: feedback.id,
    type: feedback.type,
    title: feedback.title,
    content: feedback.content,
    attachments: feedback.attachments,
    status: feedback.status,
    priority: feedback.priority,
    adminReply: feedback.adminReply,
    createdAt: feedback.createdAt,
    updatedAt: feedback.updatedAt,
    // 不包含：adminNote, assigneeId, pageUrl, userAgent
  };
}
```

---

## 🔐 安全设计

### 权限控制

```
┌─────────────────────────────────────────────────────────────────┐
│                        权限控制架构                              │
└─────────────────────────────────────────────────────────────────┘

   API 请求
      │
      ▼
┌─────────────────┐
│  JwtAuthGuard   │  验证用户身份
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ PermissionsGuard│  检查权限点
│ @RequirePermissions('feedback:read')
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ FeedbackService │  业务层数据过滤
│ 用户只能查看自己│
│ 区域管理员只能查│
│ 看本区域        │
└─────────────────┘
```

### 数据安全

- 服务端不做内容清理，展示层需转义用户输入
- 附件仅做 URL 格式与数量校验（DTO 校验）

---

## 📊 性能设计

### 数据库索引

```sql
-- platform_feedback.feedbacks 索引

-- 单字段索引
CREATE INDEX idx_feedbacks_user_id ON feedbacks(user_id);
CREATE INDEX idx_feedbacks_assignee_id ON feedbacks(assignee_id);
CREATE INDEX idx_feedbacks_status ON feedbacks(status);
CREATE INDEX idx_feedbacks_type ON feedbacks(type);
CREATE INDEX idx_feedbacks_region ON feedbacks(region);
CREATE INDEX idx_feedbacks_created_at ON feedbacks(created_at);

-- 复合索引（常用查询）
CREATE INDEX idx_feedbacks_region_status ON feedbacks(region, status);
```

### 查询优化

```typescript
// 分页查询优化
async findAll(query: FeedbackQueryDto): Promise<PaginatedResult<Feedback>> {
  const { page = 1, pageSize = 20, status, type, region, keyword } = query;
  
  const where: Prisma.FeedbackWhereInput = {
    deletedAt: null,
    ...(status && { status }),
    ...(type && { type }),
    ...(region && { region }),
    ...(keyword && {
      OR: [
        { title: { contains: keyword, mode: 'insensitive' } },
        { content: { contains: keyword, mode: 'insensitive' } }
      ]
    })
  };
  
  // 并行执行 count 和 findMany
  const [total, items] = await Promise.all([
    this.prisma.feedback.count({ where }),
    this.prisma.feedback.findMany({
      where,
      include: {
        user: { select: { id: true, displayName: true, email: true } },
        assignee: { select: { id: true, displayName: true } }
      },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * pageSize,
      take: pageSize
    })
  ]);
  
  return {
    items,
    total,
    page,
    limit: pageSize,
    totalPages: Math.ceil(total / pageSize),
    hasNext: page * pageSize < total,
    hasPrev: page > 1
  };
}
```

### 缓存策略

```typescript
// 统计数据缓存（按区域）
interface FeedbackStatsCache {
  key: `feedback:stats:${region}`;
  value: {
    total: number;
    byStatus: Record<FeedbackStatus, number>;
    byType: Record<FeedbackType, number>;
  };
  ttl: 300;  // 5分钟
}

// 缓存失效触发
const cacheInvalidationTriggers = [
  'feedback.created',
  'feedback.statusChanged',
  'feedback.deleted'
];
```

---

## 📁 目录结构

```
backend/src/modules/feedback/
├── feedback.module.ts              # 模块定义
├── feedback.controller.ts          # API 控制器
├── feedback.service.ts             # 业务逻辑
├── feedback.exceptions.ts          # 业务异常定义
├── dto/
│   └── feedback.dto.ts             # DTO 定义

frontend/src/
└── services/
    └── api/
        └── feedback.ts             # 反馈 API 客户端

prisma/schema/
└── platform_feedback.prisma        # 反馈数据模型
```

---

## 🧪 测试策略

### 单元测试

```typescript
// feedback.service.spec.ts
describe('FeedbackService', () => {
  describe('createFeedback', () => {
    it('should inherit region from user metadata', async () => {
      const user = { id: 'user-1', metadata: { region: 'US' } };
      const dto = { type: 'BUG', title: 'Test', content: 'Test content' };
      
      const feedback = await service.create(user.id, dto);
      
      expect(feedback.region).toBe('US');
      expect(feedback.status).toBe('PENDING');
    });
    
    it('should use default region if user has none', async () => {
      const user = { id: 'user-2', metadata: {} };
      const dto = { type: 'FEATURE', title: 'Test', content: 'Test' };
      
      const feedback = await service.create(user.id, dto);
      
      expect(feedback.region).toBe('CN');
    });
  });
  
  describe('updateStatus', () => {
    it('should allow valid status transition', async () => {
      const feedback = await service.updateStatus('fb-1', 'IN_PROGRESS');
      expect(feedback.status).toBe('IN_PROGRESS');
    });
    
    it('should reject invalid status transition', async () => {
      // PENDING -> RESOLVED 不允许直接跳转
      await expect(
        service.updateStatus('fb-1', 'RESOLVED')
      ).rejects.toThrow('FEEDBACK_INVALID_STATUS_TRANSITION');
    });
  });
  
  describe('region filtering', () => {
    it('should filter by region for regional admin', async () => {
      const user = { roles: ['FEEDBACK_ADMIN_CN'] };
      const feedbacks = await service.findAll({}, user);
      
      feedbacks.items.forEach(fb => {
        expect(fb.region).toBe('CN');
      });
    });
    
    it('should not filter for global admin', async () => {
      const user = { roles: ['ADMINISTRATOR'] };
      const feedbacks = await service.findAll({}, user);
      
      const regions = [...new Set(feedbacks.items.map(fb => fb.region))];
      expect(regions.length).toBeGreaterThan(1);
    });
  });
});
```

### 集成测试

```typescript
// feedback.integration.spec.ts
describe('Feedback API (e2e)', () => {
  describe('POST /feedbacks', () => {
    it('should create feedback for authenticated user', async () => {
      const response = await request(app.getHttpServer())
        .post('/feedbacks')
        .set('Authorization', `Bearer ${userToken}`)
        .send({
          type: 'BUG',
          title: '登录页面无法加载',
          content: '点击登录按钮后页面一直转圈'
        })
        .expect(201);
      
      expect(response.body.success).toBe(true);
      expect(response.body.data.status).toBe('PENDING');
    });
    
    it('should reject unauthenticated request', async () => {
      await request(app.getHttpServer())
        .post('/feedbacks')
        .send({ type: 'BUG', title: 'Test', content: 'Test' })
        .expect(401);
    });
  });
  
  describe('GET /feedbacks/my', () => {
    it('should only return user own feedbacks', async () => {
      const response = await request(app.getHttpServer())
        .get('/feedbacks/my')
        .set('Authorization', `Bearer ${userToken}`)
        .expect(200);
      
      response.body.data.items.forEach(fb => {
        expect(fb.userId).toBe(testUserId);
      });
    });
  });
  
  describe('GET /feedbacks (admin)', () => {
    it('should filter by region for regional admin', async () => {
      const response = await request(app.getHttpServer())
        .get('/feedbacks')
        .set('Authorization', `Bearer ${cnAdminToken}`)
        .expect(200);
      
      response.body.data.items.forEach(fb => {
        expect(fb.region).toBe('CN');
      });
    });
  });
});
```

---

## 📚 相关文档

- [PRD - 产品需求文档](./01-prd.md)
- [API - 接口文档](./07-api.md)
- [后端规范入口](../../../.agents/skills/backend-main/references/backend-standards.md)
- [用户与组织架构管理](../organization/README.md)

---

**最后更新**: 2026-01-07  
**版本**: v1.0.3
