# 邀请码验证功能设计方案

## 一、整体流程

```
用户首次使用 → 检查邀请码状态 → 未验证则弹窗输入 → 后端验证 → 验证通过后可正常对话
```

## 二、前端设计

### 2.1 邀请码状态管理

在 `ExtensionContext.globalState` 中存储邀请码验证状态：

```typescript
// 存储结构
{
  "invitationCodeVerified": true,
  "invitationCode": "INVITE2024ABC",
  "verifiedTime": "2024-01-20T10:30:00"
}
```

### 2.2 UI 交互流程

#### 弹窗输入邀请码
使用 `vscode.window.showInputBox` 实现：

```typescript
const invitationCode = await vscode.window.showInputBox({
  prompt: '请输入邀请码以继续使用 IC Coder',
  placeHolder: '例如：INVITE2024ABC',
  ignoreFocusOut: true,
  validateInput: (value) => {
    if (!value || value.trim().length === 0) {
      return '邀请码不能为空';
    }
    if (value.length < 6) {
      return '邀请码格式不正确';
    }
    return null;
  }
});
```

#### 验证结果提示
- 成功：`vscode.window.showInformationMessage('邀请码验证成功！')`
- 失败：`vscode.window.showErrorMessage('邀请码无效或已过期，请重新输入')`

### 2.3 验证时机

在以下场景触发邀请码验证：

1. **用户首次发送消息时**（在 `handleUserMessage` 中检查）
2. **用户登录后**（在登录成功回调中检查）
3. **Token 过期重新登录后**

### 2.4 前端验证流程图

```
发送消息前检查
├─ 检查是否已登录
│  └─ 未登录 → 提示登录
├─ 检查邀请码是否已验证
│  ├─ 未验证
│  │  ├─ 弹窗输入邀请码
│  │  ├─ 调用后端验证接口 POST /api/invitation/verify
│  │  ├─ 验证成功
│  │  │  ├─ 保存验证状态到 globalState
│  │  │  └─ 继续发送消息
│  │  └─ 验证失败
│  │     ├─ 显示错误提示
│  │     └─ 阻止发送消息
│  └─ 已验证 → 继续发送消息
```

### 2.5 前端文件修改清单

#### 新增文件

**`src/services/invitationService.ts`** - 邀请码服务
```typescript
/**
 * 邀请码验证服务
 */
export class InvitationService {
  /**
   * 检查用户是否已验证邀请码
   */
  static async isVerified(context: vscode.ExtensionContext): Promise<boolean>

  /**
   * 验证邀请码
   */
  static async verifyCode(code: string): Promise<boolean>

  /**
   * 保存验证状态
   */
  static async saveVerificationStatus(
    context: vscode.ExtensionContext,
    code: string
  ): Promise<void>

  /**
   * 清除验证状态（用于退出登录）
   */
  static async clearVerificationStatus(
    context: vscode.ExtensionContext
  ): Promise<void>

  /**
   * 显示邀请码输入弹窗
   */
  static async showInputDialog(): Promise<string | undefined>
}
```

#### 修改文件

**`src/utils/messageHandler.ts`**
- 在 `handleUserMessage` 函数开头添加邀请码验证检查

**`src/services/apiClient.ts`**
- 添加 `verifyInvitationCode` 函数
- 添加 `checkInvitationStatus` 函数

**`src/types/api.ts`**
- 添加邀请码相关类型定义

**`src/panels/ICHelperPanel.ts`**
- 在面板创建时检查邀请码状态（可选）

**`src/extension.ts`**
- 在登录成功后检查邀请码状态

### 2.6 类型定义

在 `src/types/api.ts` 中添加：

```typescript
// ============== 邀请码验证 ==============

/**
 * 邀请码验证请求
 * POST /api/invitation/verify
 */
export interface InvitationVerifyRequest {
  /** 邀请码 */
  code: string;
}

/**
 * 邀请码验证响应
 */
export interface InvitationVerifyResponse {
  /** 响应代码 */
  code: number;
  /** 响应消息 */
  msg: string;
  /** 验证结果数据 */
  data?: {
    /** 是否验证成功 */
    verified: boolean;
  };
}

/**
 * 邀请码状态响应
 * GET /api/invitation/status
 */
export interface InvitationStatusResponse {
  /** 响应代码 */
  code: number;
  /** 响应消息 */
  msg?: string;
  /** 状态数据 */
  data?: {
    /** 是否已验证 */
    verified: boolean;
    /** 使用的邀请码 */
    invitationCode?: string;
    /** 验证时间 */
    verifiedTime?: string;
  };
}
```

## 三、后端设计

### 3.1 数据库设计

#### 邀请码表 (invitation_codes)

```sql
CREATE TABLE invitation_codes (
  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
  code VARCHAR(32) UNIQUE NOT NULL COMMENT '邀请码',
  max_uses INT DEFAULT 1 COMMENT '最大使用次数，-1表示无限制',
  used_count INT DEFAULT 0 COMMENT '已使用次数',
  expire_time DATETIME COMMENT '过期时间，NULL表示永不过期',
  created_by BIGINT COMMENT '创建者用户ID',
  created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  status TINYINT DEFAULT 1 COMMENT '状态：1-有效，0-禁用',
  remark VARCHAR(500) COMMENT '备注',
  INDEX idx_code (code),
  INDEX idx_status (status),
  INDEX idx_expire_time (expire_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表';
```

#### 用户邀请码关联表 (user_invitation)

```sql
CREATE TABLE user_invitation (
  id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
  user_id BIGINT NOT NULL COMMENT '用户ID',
  invitation_code VARCHAR(32) NOT NULL COMMENT '使用的邀请码',
  verified_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '验证时间',
  ip_address VARCHAR(50) COMMENT '验证时的IP地址',
  UNIQUE KEY uk_user_id (user_id),
  INDEX idx_invitation_code (invitation_code),
  INDEX idx_verified_time (verified_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请码关联表';
```

### 3.2 API 接口设计

#### 3.2.1 验证邀请码

**接口地址**：`POST /api/invitation/verify`

**请求头**：
```
Authorization: Bearer {token}
Content-Type: application/json
```

**请求体**：
```json
{
  "code": "INVITE2024ABC"
}
```

**响应示例**：

成功：
```json
{
  "code": 200,
  "msg": "验证成功",
  "data": {
    "verified": true
  }
}
```

失败：
```json
{
  "code": 400,
  "msg": "邀请码无效或已过期"
}
```

```json
{
  "code": 400,
  "msg": "邀请码使用次数已达上限"
}
```

```json
{
  "code": 400,
  "msg": "您已验证过邀请码，无需重复验证"
}
```

#### 3.2.2 查询验证状态

**接口地址**：`GET /api/invitation/status`

**请求头**：
```
Authorization: Bearer {token}
```

**响应示例**：

已验证：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "verified": true,
    "invitationCode": "INVITE2024ABC",
    "verifiedTime": "2024-01-20T10:30:00"
  }
}
```

未验证：
```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "verified": false
  }
}
```

#### 3.2.3 管理接口（管理员使用）

**生成邀请码**：`POST /api/admin/invitation/generate`

请求体：
```json
{
  "count": 10,
  "maxUses": 1,
  "expireTime": "2024-12-31T23:59:59",
  "remark": "2024年1月批次"
}
```

响应：
```json
{
  "code": 200,
  "msg": "生成成功",
  "data": {
    "codes": [
      "INVITE2024001",
      "INVITE2024002",
      "..."
    ]
  }
}
```

**查询邀请码列表**：`GET /api/admin/invitation/list`

**禁用邀请码**：`PUT /api/admin/invitation/disable/{code}`

**查询使用记录**：`GET /api/admin/invitation/usage/{code}`

### 3.3 后端验证逻辑

#### 验证流程

```java
public boolean verifyInvitationCode(Long userId, String code) {
    // 1. 检查邀请码是否存在
    InvitationCode invitationCode = invitationCodeMapper.selectByCode(code);
    if (invitationCode == null) {
        throw new BusinessException("邀请码不存在");
    }

    // 2. 检查邀请码状态
    if (invitationCode.getStatus() != 1) {
        throw new BusinessException("邀请码已被禁用");
    }

    // 3. 检查是否过期
    if (invitationCode.getExpireTime() != null
        && invitationCode.getExpireTime().before(new Date())) {
        throw new BusinessException("邀请码已过期");
    }

    // 4. 检查使用次数
    if (invitationCode.getMaxUses() != -1
        && invitationCode.getUsedCount() >= invitationCode.getMaxUses()) {
        throw new BusinessException("邀请码使用次数已达上限");
    }

    // 5. 检查用户是否已验证过
    UserInvitation existing = userInvitationMapper.selectByUserId(userId);
    if (existing != null) {
        throw new BusinessException("您已验证过邀请码，无需重复验证");
    }

    // 6. 创建用户验证记录
    UserInvitation userInvitation = new UserInvitation();
    userInvitation.setUserId(userId);
    userInvitation.setInvitationCode(code);
    userInvitation.setVerifiedTime(new Date());
    userInvitationMapper.insert(userInvitation);

    // 7. 增加邀请码使用次数
    invitationCodeMapper.incrementUsedCount(code);

    return true;
}
```

### 3.4 权限拦截

在对话接口中添加邀请码验证拦截：

```java
@PostMapping("/dialog/stream")
public SseEmitter dialog(@RequestBody DialogRequest request) {
    // 获取当前用户ID
    Long userId = SecurityUtils.getUserId();

    // 检查用户是否已验证邀请码
    if (!invitationService.isUserVerified(userId)) {
        throw new BusinessException("请先验证邀请码后再使用对话功能");
    }

    // 继续处理对话请求
    // ...
}
```

或者使用拦截器统一处理：

```java
@Component
public class InvitationInterceptor implements HandlerInterceptor {

    @Autowired
    private InvitationService invitationService;

    @Override
    public boolean preHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler) throws Exception {
        // 获取当前用户ID
        Long userId = SecurityUtils.getUserId();

        // 检查是否需要验证邀请码的接口
        String uri = request.getRequestURI();
        if (needsInvitationVerification(uri)) {
            if (!invitationService.isUserVerified(userId)) {
                throw new BusinessException("请先验证邀请码");
            }
        }

        return true;
    }

    private boolean needsInvitationVerification(String uri) {
        // 需要验证邀请码的接口列表
        return uri.startsWith("/api/dialog/")
            || uri.startsWith("/api/task/");
    }
}
```

### 3.5 后端文件清单

#### 实体类
- `com.iccoder.entity.InvitationCode` - 邀请码实体
- `com.iccoder.entity.UserInvitation` - 用户邀请码关联实体

#### Mapper
- `com.iccoder.mapper.InvitationCodeMapper` - 邀请码数据访问
- `com.iccoder.mapper.UserInvitationMapper` - 用户邀请码关联数据访问

#### Service
- `com.iccoder.service.InvitationService` - 邀请码业务逻辑
- `com.iccoder.service.impl.InvitationServiceImpl` - 实现类

#### Controller
- `com.iccoder.controller.InvitationController` - 邀请码接口
- `com.iccoder.controller.admin.InvitationAdminController` - 管理接口

#### 拦截器
- `com.iccoder.interceptor.InvitationInterceptor` - 邀请码验证拦截器

## 四、用户体验优化

### 4.1 首次使用引导

在用户首次打开聊天面板时，如果未验证邀请码，显示友好的引导信息：

```typescript
// 在 ICHelperPanel.ts 中
if (!await InvitationService.isVerified(context)) {
  panel.webview.postMessage({
    command: 'showInvitationGuide',
    message: '欢迎使用 IC Coder！请先输入邀请码以开始使用。'
  });
}
```

### 4.2 状态持久化

验证状态保存在 `globalState` 中，避免重复验证：

```typescript
// 保存验证状态
await context.globalState.update('invitationCodeVerified', true);
await context.globalState.update('invitationCode', code);
await context.globalState.update('verifiedTime', new Date().toISOString());
```

### 4.3 错误提示优化

根据不同的错误类型，提供清晰的提示信息：

| 错误类型 | 提示信息 |
|---------|---------|
| 邀请码不存在 | "邀请码不存在，请检查后重新输入" |
| 邀请码已过期 | "邀请码已过期，请联系管理员获取新的邀请码" |
| 使用次数已达上限 | "该邀请码使用次数已达上限，请使用其他邀请码" |
| 已验证过 | "您已验证过邀请码，无需重复验证" |
| 网络错误 | "网络连接失败，请检查网络后重试" |

### 4.4 支持重新验证

提供命令允许用户更换邀请码：

```typescript
// 在 extension.ts 中注册命令
context.subscriptions.push(
  vscode.commands.registerCommand('ic-coder.changeInvitationCode', async () => {
    const confirm = await vscode.window.showWarningMessage(
      '确定要更换邀请码吗？',
      '确定',
      '取消'
    );

    if (confirm === '确定') {
      await InvitationService.clearVerificationStatus(context);
      vscode.window.showInformationMessage('已清除邀请码，请重新验证');
    }
  })
);
```

### 4.5 Webview 中显示状态（可选）

在聊天界面顶部显示邀请码验证状态：

```html
<!-- 已验证 -->
<div class="invitation-status verified">
  <span class="icon">✓</span>
  <span>邀请码已验证</span>
</div>

<!-- 未验证 -->
<div class="invitation-status unverified">
  <span class="icon">!</span>
  <span>请先验证邀请码</span>
  <button onclick="verifyInvitationCode()">立即验证</button>
</div>
```

## 五、安全考虑

### 5.1 邀请码生成规则

使用安全的随机算法生成邀请码：

```java
public String generateInvitationCode() {
    // 使用 UUID + 时间戳 + 随机数
    String uuid = UUID.randomUUID().toString().replace("-", "");
    String timestamp = String.valueOf(System.currentTimeMillis());
    String random = RandomStringUtils.randomAlphanumeric(6);

    // 组合并取前16位
    String combined = uuid + timestamp + random;
    String code = DigestUtils.sha256Hex(combined).substring(0, 16).toUpperCase();

    return "IC" + code; // 添加前缀，例如：IC3F2A9B1C4D5E6F
}
```

### 5.2 防暴力破解

限制验证频率，添加验证失败次数限制：

```java
// 使用 Redis 记录验证失败次数
String key = "invitation:fail:" + userId;
Integer failCount = redisTemplate.opsForValue().get(key);

if (failCount != null && failCount >= 5) {
    throw new BusinessException("验证失败次数过多，请1小时后再试");
}

// 验证失败时增加计数
if (!verifySuccess) {
    redisTemplate.opsForValue().increment(key);
    redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
```

### 5.3 Token 绑定

邀请码验证状态与用户 Token 绑定，退出登录时清除：

```typescript
// 在退出登录时清除验证状态
vscode.commands.registerCommand('ic-coder.logout', async () => {
  // 清除 session
  await clearSession();

  // 清除邀请码验证状态
  await InvitationService.clearVerificationStatus(context);

  vscode.window.showInformationMessage('已退出登录');
});
```

### 5.4 日志记录

记录所有验证尝试，便于审计和分析：

```java
@Slf4j
public class InvitationServiceImpl implements InvitationService {

    @Override
    public boolean verifyInvitationCode(Long userId, String code) {
        log.info("用户 {} 尝试验证邀请码: {}", userId, code);

        try {
            // 验证逻辑
            // ...

            log.info("用户 {} 验证邀请码成功: {}", userId, code);
            return true;
        } catch (Exception e) {
            log.warn("用户 {} 验证邀请码失败: {}, 原因: {}",
                     userId, code, e.getMessage());
            throw e;
        }
    }
}
```

### 5.5 敏感信息保护

- 邀请码在数据库中可以考虑加密存储（可选）
- API 响应中不暴露邀请码的详细信息（如剩余次数）
- 前端不缓存邀请码明文，只保存验证状态

## 六、实施步骤

### 阶段一：后端开发（优先）

1. 创建数据库表
2. 实现邀请码生成和管理功能
3. 实现验证接口
4. 添加权限拦截
5. 测试接口功能

### 阶段二：前端开发

1. 添加类型定义
2. 实现 `InvitationService`
3. 修改 `apiClient.ts` 添加接口调用
4. 修改 `messageHandler.ts` 添加验证检查
5. 测试完整流程

### 阶段三：联调测试

1. 前后端联调
2. 测试各种异常场景
3. 优化用户体验
4. 性能测试

### 阶段四：上线部署

1. 生成初始邀请码
2. 更新用户文档
3. 灰度发布
4. 监控运行状态

## 七、测试用例

### 7.1 正常流程测试

| 测试场景 | 预期结果 |
|---------|---------|
| 首次使用，输入有效邀请码 | 验证成功，可以正常对话 |
| 已验证用户再次打开面板 | 无需重复验证，直接使用 |
| 退出登录后重新登录 | 需要重新验证邀请码 |

### 7.2 异常场景测试

| 测试场景 | 预期结果 |
|---------|---------|
| 输入不存在的邀请码 | 提示"邀请码不存在" |
| 输入已过期的邀请码 | 提示"邀请码已过期" |
| 输入使用次数已满的邀请码 | 提示"使用次数已达上限" |
| 已验证用户尝试再次验证 | 提示"已验证过，无需重复验证" |
| 网络断开时验证 | 提示"网络连接失败" |
| 连续输入错误邀请码5次 | 提示"验证失败次数过多，请稍后再试" |

### 7.3 边界条件测试

| 测试场景 | 预期结果 |
|---------|---------|
| 邀请码为空 | 前端验证拦截，提示"邀请码不能为空" |
| 邀请码长度不足 | 前端验证拦截，提示"邀请码格式不正确" |
| 邀请码包含特殊字符 | 后端验证失败，提示"邀请码不存在" |
| 同一邀请码多人同时使用 | 使用数据库锁，确保不超过最大次数 |

## 八、FAQ

### Q1: 用户忘记邀请码怎么办？
A: 邀请码验证成功后，用户无需记住邀请码。如果需要查看，可以在设置中显示已验证的邀请码。

### Q2: 邀请码可以重复使用吗？
A: 取决于邀请码的 `maxUses` 设置。可以设置为 1（一次性）、N（限定次数）或 -1（无限制）。

### Q3: 如何批量生成邀请码？
A: 使用管理接口 `POST /api/admin/invitation/generate`，指定生成数量即可。

### Q4: 邀请码验证失败会影响登录吗？
A: 不会。邀请码验证是独立的，只影响对话功能的使用，不影响登录。

### Q5: 可以为不同用户群体设置不同的邀请码吗？
A: 可以。通过 `remark` 字段标记不同批次的邀请码，便于管理和统计。

## 九、后续优化方向

1. **邀请码分级**：不同等级的邀请码对应不同的权限（如对话次数、模型选择等）
2. **邀请奖励**：邀请他人使用可获得积分或额外权限
3. **邀请统计**：统计每个邀请码的使用情况和用户活跃度
4. **自动过期**：根据使用情况自动延长或缩短邀请码有效期
5. **白名单机制**：特定用户可以免邀请码使用

---

**文档版本**：v1.0
**最后更新**：2026-01-27
**维护者**：IC Coder Team
