# 插件试用用户功能实现方案

## 1. 方案概述

**核心思路：**
- Web 登录成功后只返回 token（保持现状）
- 插件调用 `getUserInfo(token)` 时，后端返回的数据里包含标识字段
- 前端根据该字段判断是否是插件试用用户
- 插件试用用户：显示欢迎弹窗，不显示邀请码弹窗
- 正式用户：显示邀请码弹窗（现有逻辑）

---

## 2. 后端需要做什么

### 2.1 在用户信息接口中添加字段

**接口：** `GET /system/user/getInfo`

**现有响应：**
```json
{
  "userId": "xxx",
  "username": "testuser",
  "nickname": "测试用户",
  "email": "test@example.com"
}
```

**新增字段：**

**方案 ：添加 isPluginTrial 字段**

```json
{
  "userId": "xxx",
  "username": "testuser",
  "nickname": "测试用户",
  "email": "test@example.com",
  "isPluginTrial": true,        // ← 新增：是否是插件试用用户
  "pluginTrialExpiresAt": 1709654400000  // ← 新增：试用到期时间（毫秒时间戳）
}
```

### 2.2 后端逻辑说明

**判断逻辑：**
```javascript
// 伪代码
function getUserInfo(userId) {
  const user = db.users.findById(userId);

  // 判断是否是插件试用用户（后端自己的逻辑）
  const isPluginTrial = checkIfPluginTrialUser(user);

  return {
    userId: user.id,
    username: user.username,
    nickname: user.nickname,
    email: user.email,
    isPluginTrial: isPluginTrial,
    pluginTrialExpiresAt: isPluginTrial ? user.trial_expires_at : null
  };
}
```

**Token 过期时间：**
- 插件试用用户：JWT Token 设置 15 天过期
- 正式用户：JWT Token 设置 30 天过期（或现有逻辑）

---

## 3. 前端需要修改的地方

### 3.1 修改 UserInfo 接口

**文件：** `src/services/userService.ts`

**现有接口：**
```typescript
interface UserInfo {
  userId: string;
  username: string;
  nickname: string;
  email?: string;
  phonenumber?: string;
  avatar?: string;
  roles?: string[];
  permissions?: string[];
  createTime?: string;
  loginDate?: string;
  membership?: {
    tierCode: string;
    tierName: string;
    tierLevel: number;
    remainingDays?: number;
    monthlyCredits?: number;
  };
  credits?: number;
}
```

**新增字段：**
```typescript
interface UserInfo {
  // ... 现有字段
  isPluginTrial?: boolean;        // ← 新增：是否是插件试用用户
  pluginTrialExpiresAt?: number;  // ← 新增：试用到期时间（毫秒时间戳）
  membership?: {
    tierCode: string;
    tierName: string;
    tierLevel: number;
    remainingDays?: number;
    monthlyCredits?: number;
  };
  credits?: number;
}
```


### 3.2 修改 onTokenReceived() 方法

**文件：** `src/services/userService.ts`

**现有代码位置：** 约 200 行左右

**修改内容：**
```typescript
async onTokenReceived(token: string) {
  // 现有逻辑：并行获取三类信息
  const [userInfo, membershipInfo, credits] = await Promise.all([
    getUserInfo(token),
    getMembershipInfo(token),
    fetchBalanceWithToken(token)
  ]);

  // 合并数据
  const fullUserInfo = {
    ...userInfo,
    membership: membershipInfo,
    credits: credits
  };

  // 保存到 globalState
  await this.context.globalState.update('icCoderUserInfo', fullUserInfo);

  // ========== 新增逻辑 ==========
  // 判断是否是插件试用用户
  if (fullUserInfo.isPluginTrial === true) {
    // 插件试用用户：显示欢迎弹窗，不显示邀请码弹窗
    await this.showWelcomePanel();
    // 标记为已显示欢迎弹窗（避免重复显示）
    await this.context.globalState.update('pluginTrialWelcomed', true);
  } else {
    // 正式用户：显示邀请码弹窗（现有逻辑）
    await this.checkAndShowInvitationModal();
  }
  // ========== 新增逻辑结束 ==========

  return fullUserInfo;
}
```


### 3.3 新增欢迎弹窗面板

**新建文件：** `src/panels/WelcomePanel.ts`

```typescript
/**
 * 欢迎引导面板
 * 功能：插件试用用户首次登录显示使用教程
 */

import * as vscode from 'vscode';

export class WelcomePanel {
  public static currentPanel: WelcomePanel | undefined;
  private readonly _panel: vscode.WebviewPanel;
  private _disposables: vscode.Disposable[] = [];

  private constructor(panel: vscode.WebviewPanel) {
    this._panel = panel;
    this._panel.webview.html = this.getHtmlContent();
    
    // 监听关闭事件
    this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
  }

  public static render(context: vscode.ExtensionContext) {
    // 避免重复显示
    if (WelcomePanel.currentPanel) {
      WelcomePanel.currentPanel._panel.reveal(vscode.ViewColumn.One);
      return;
    }

    const panel = vscode.window.createWebviewPanel(
      'icCoderWelcome',
      '欢迎使用 IC Coder',
      vscode.ViewColumn.One,
      {
        enableScripts: true,
        retainContextWhenHidden: true
      }
    );

    WelcomePanel.currentPanel = new WelcomePanel(panel);
  }

  private getHtmlContent(): string {
    return `
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>欢迎使用 IC Coder</title>
        <style>
          body {
            padding: 40px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            line-height: 1.6;
            color: var(--vscode-foreground);
            background-color: var(--vscode-editor-background);
          }
          h1 {
            color: var(--vscode-textLink-foreground);
            margin-bottom: 20px;
          }
          .welcome-message {
            font-size: 16px;
            margin-bottom: 30px;
            color: var(--vscode-descriptionForeground);
          }
          .step {
            margin: 20px 0;
            padding: 20px;
            background: var(--vscode-editor-inactiveSelectionBackground);
            border-radius: 8px;
            border-left: 4px solid var(--vscode-textLink-foreground);
          }
          .step h3 {
            margin-top: 0;
            color: var(--vscode-textLink-foreground);
          }
          .step p {
            margin: 10px 0;
          }
          .button {
            padding: 12px 24px;
            background: var(--vscode-button-background);
            color: var(--vscode-button-foreground);
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-top: 20px;
          }
          .button:hover {
            background: var(--vscode-button-hoverBackground);
          }
        </style>
      </head>
      <body>
        <h1>🎉 欢迎使用 IC Coder！</h1>
        <p class="welcome-message">
          您已成功激活 15 天试用期，让我们开始探索 IC Coder 的强大功能吧！
        </p>
        
        <div class="step">
          <h3>📝 步骤 1：打开聊天面板</h3>
          <p>点击侧边栏的 IC Coder 图标，或使用命令面板搜索 "IC Coder: Open Chat"</p>
        </div>
        
        <div class="step">
          <h3>💬 步骤 2：输入您的需求</h3>
          <p>描述您想要生成的 Verilog 代码或需要帮助的问题，AI 将为您提供专业的解决方案</p>
        </div>
        
        <div class="step">
          <h3>🔬 步骤 3：运行仿真</h3>
          <p>使用 "生成 VCD" 命令运行 iverilog 仿真，并通过波形查看器查看仿真结果</p>
        </div>
        
        <button class="button" onclick="close()">开始使用</button>
        
        <script>
          function close() {
            // 通知 VS Code 关闭面板
            window.close();
          }
        </script>
      </body>
      </html>
    `;
  }

  public dispose() {
    WelcomePanel.currentPanel = undefined;
    this._panel.dispose();
    while (this._disposables.length) {
      const disposable = this._disposables.pop();
      if (disposable) {
        disposable.dispose();
      }
    }
  }
}
```


### 3.4 新增过期提醒面板

**新建文件：** `src/panels/ExpiredPanel.ts`

```typescript
/**
 * 试用期到期提醒面板
 * 功能：试用期到期时显示续费提示
 */

import * as vscode from 'vscode';

export class ExpiredPanel {
  public static render() {
    const panel = vscode.window.createWebviewPanel(
      'icCoderExpired',
      '试用期已到期',
      vscode.ViewColumn.One,
      { enableScripts: true }
    );

    panel.webview.html = this.getHtmlContent();
  }

  private static getHtmlContent(): string {
    return `
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
        <meta charset="UTF-8">
        <style>
          body {
            padding: 60px 40px;
            text-align: center;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            color: var(--vscode-foreground);
            background-color: var(--vscode-editor-background);
          }
          h1 {
            color: var(--vscode-errorForeground);
            font-size: 28px;
            margin-bottom: 20px;
          }
          p {
            font-size: 16px;
            line-height: 1.6;
            margin: 15px 0;
            color: var(--vscode-descriptionForeground);
          }
          .button {
            padding: 12px 30px;
            background: var(--vscode-button-background);
            color: var(--vscode-button-foreground);
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin: 10px;
          }
          .button:hover {
            background: var(--vscode-button-hoverBackground);
          }
        </style>
      </head>
      <body>
        <h1>⏰ 您的试用期已到期</h1>
        <p>感谢您使用 IC Coder！您的 15 天试用期已结束。</p>
        <p>如需继续使用，请联系我们获取正式版本。</p>
        
        <button class="button" onclick="contact()">联系我们</button>
        
        <script>
          function contact() {
            // 可以打开联系页面或发送邮件
            window.open('https://iccoder.com/contact', '_blank');
          }
        </script>
      </body>
      </html>
    `;
  }
}
```


### 3.5 新增过期检测服务

**新建文件：** `src/services/trialExpirationService.ts`

```typescript
/**
 * 试用期过期检测服务
 * 功能：检查插件试用用户是否过期
 */

import * as vscode from 'vscode';
import { getUserInfo } from './userService';
import { ExpiredPanel } from '../panels/ExpiredPanel';

export class TrialExpirationService {
  private context: vscode.ExtensionContext;

  constructor(context: vscode.ExtensionContext) {
    this.context = context;
  }

  /**
   * 检查是否过期
   * @returns true=已过期，false=未过期
   */
  public async checkExpiration(): Promise<boolean> {
    const userInfo = await getUserInfo();
    
    // 不是插件试用用户，不需要检查
    if (!userInfo?.isPluginTrial) {
      return false;
    }

    // 没有过期时间，不检查
    if (!userInfo.pluginTrialExpiresAt) {
      return false;
    }

    // 检查是否过期
    const now = Date.now();
    if (now >= userInfo.pluginTrialExpiresAt) {
      // 已过期
      await this.handleExpired();
      return true;
    }

    return false;
  }

  /**
   * 处理过期逻辑
   */
  private async handleExpired(): Promise<void> {
    // 显示过期弹窗
    ExpiredPanel.render();

    // 清除本地数据（可选）
    // await this.context.globalState.update('icCoderUserInfo', undefined);
    // await this.context.globalState.update('icCoderSessions', undefined);
  }
}
```


### 3.6 在消息发送前检查过期

**文件：** `src/utils/messageHandler.ts`

**修改位置：** 在发送消息给后端之前添加过期检查

```typescript
import { TrialExpirationService } from '../services/trialExpirationService';

// 在 handleUserMessage 或类似的消息处理函数中添加
async function handleUserMessage(message: string, context: vscode.ExtensionContext) {
  // ========== 新增：检查试用期是否过期 ==========
  const trialService = new TrialExpirationService(context);
  const isExpired = await trialService.checkExpiration();
  
  if (isExpired) {
    // 已过期，禁止使用
    return {
      success: false,
      message: '您的试用期已到期，请联系我们获取正式版本'
    };
  }
  // ========== 新增结束 ==========

  // 现有的消息处理逻辑
  // ...
}
```


---

## 4. 完整的实现流程

### 4.1 登录流程（带过期检查）

```
1. 用户点击登录
   ↓
2. 打开浏览器，Web 端登录
   ↓
3. 重定向回插件：http://localhost:{port}/callback?token={token}
   ↓
4. 插件调用 onTokenReceived(token)
   ↓
5. 并行获取：getUserInfo + getMembershipInfo + Credits
   ↓
6. 后端返回 userInfo（包含 isPluginTrial 和 pluginTrialExpiresAt）
   ↓
7. 判断 isPluginTrial === true？
   ├─ 是：显示欢迎弹窗，不显示邀请码弹窗
   └─ 否：显示邀请码弹窗（现有逻辑）
   ↓
8. 保存用户信息到 globalState
```

### 4.2 使用功能时的过期检查

```
1. 用户发送消息/使用功能
   ↓
2. 调用 trialService.checkExpiration()
   ↓
3. 获取 userInfo，检查 isPluginTrial
   ↓
4. 如果是插件试用用户，检查 Date.now() >= pluginTrialExpiresAt？
   ├─ 是：显示过期弹窗，禁止使用，返回 true
   └─ 否：允许使用，返回 false
   ↓
5. 继续正常的消息处理流程
```


---

## 5. ⚠️ 潜在 Bug 和注意事项

### 5.1 时间同步问题

**问题：** 前端使用 `Date.now()` 判断过期，如果用户本地时间不准确会导致误判

**场景：**
- 用户本地时间快了 1 天 → 提前显示过期弹窗
- 用户本地时间慢了 1 天 → 过期后仍可使用

**解决方案：**
```typescript
// 方案 1：每次使用前调用后端验证（推荐）
async checkExpiration(): Promise<boolean> {
  try {
    // 调用后端接口验证 Token 是否过期
    const response = await fetch(`${API_BASE}/auth/verify`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    if (response.status === 401) {
      // Token 过期
      await this.handleExpired();
      return true;
    }
    return false;
  } catch (error) {
    // 网络错误，使用本地时间判断
    const userInfo = await getUserInfo();
    return Date.now() >= (userInfo?.pluginTrialExpiresAt || 0);
  }
}
```


### 5.2 欢迎弹窗重复显示

**问题：** 每次登录都显示欢迎弹窗，用户体验不好

**场景：**
- 试用用户第一次登录显示欢迎弹窗 ✅
- 用户关闭插件后重新打开，又显示欢迎弹窗 ❌

**解决方案：**
```typescript
async onTokenReceived(token: string) {
  // ... 获取用户信息

  if (fullUserInfo.isPluginTrial === true) {
    // 检查是否已经显示过欢迎弹窗
    const hasWelcomed = this.context.globalState.get('pluginTrialWelcomed');
    
    if (!hasWelcomed) {
      await this.showWelcomePanel();
      await this.context.globalState.update('pluginTrialWelcomed', true);
    }
  } else {
    await this.checkAndShowInvitationModal();
  }
}
```


### 5.3 isPluginTrial 字段类型不一致

**问题：** 后端可能返回 `true`、`false`、`null`、`undefined`，前端判断时需要严格处理

**场景：**
```typescript
// ❌ 错误写法
if (userInfo.isPluginTrial) {
  // undefined 会被判断为 false
}

// ✅ 正确写法
if (userInfo.isPluginTrial === true) {
  // 插件试用用户
}
```

**建议：**
- 后端统一返回 `true` 或 `false`，不要返回 `null`
- 前端使用严格相等 `===` 判断


### 5.4 Token 过期但前端未清除

**问题：** Token 在后端已过期，但前端仍保存着过期的 Token

**场景：**
- 用户 15 天后打开插件
- 前端尝试调用 API，后端返回 401
- 前端没有处理 401，导致功能异常

**解决方案：**
```typescript
// 在 apiClient.ts 中统一处理 401
async function apiCall(url: string, options: any) {
  const response = await fetch(url, options);

  if (response.status === 401) {
    // Token 过期，清除本地数据
    await clearAllData();
    ExpiredPanel.render();
    throw new Error('Token expired');
  }

  return response.json();
}
```


---

## 6. 前后端对接清单

### 6.1 后端需要提供

**1. 修改 `GET /system/user/getInfo` 接口**
- 新增字段：`isPluginTrial` (boolean)
- 新增字段：`pluginTrialExpiresAt` (number, 毫秒时间戳)

**2. Token 过期时间设置**
- 插件试用用户：JWT Token 设置 15 天过期
- 正式用户：保持现有逻辑

**3. 测试账号**
- 提供 1-2 个插件试用用户账号用于测试

### 6.2 前端需要修改

**1. 修改文件：**
- `src/services/userService.ts` - 修改 UserInfo 接口和 onTokenReceived()
- `src/utils/messageHandler.ts` - 添加过期检查

**2. 新增文件：**
- `src/panels/WelcomePanel.ts` - 欢迎弹窗
- `src/panels/ExpiredPanel.ts` - 过期提醒弹窗
- `src/services/trialExpirationService.ts` - 过期检测服务

**3. globalState 新增存储键：**
- `pluginTrialWelcomed` (boolean) - 是否已显示欢迎弹窗


---

## 7. 测试计划

### 7.1 登录流程测试

**测试用例 1：插件试用用户登录**
- 使用插件试用用户账号登录
- 验证是否显示欢迎弹窗
- 验证是否不显示邀请码弹窗
- 验证 userInfo 中 isPluginTrial === true

**测试用例 2：正式用户登录**
- 使用正式用户账号登录
- 验证是否显示邀请码弹窗
- 验证是否不显示欢迎弹窗
- 验证 userInfo 中 isPluginTrial !== true

**测试用例 3：欢迎弹窗不重复显示**
- 插件试用用户登录后显示欢迎弹窗
- 关闭插件重新打开
- 验证不再显示欢迎弹窗


### 7.2 过期检测测试

**测试用例 4：未过期用户正常使用**
- 插件试用用户登录（未过期）
- 发送消息使用功能
- 验证功能正常使用

**测试用例 5：已过期用户禁止使用**
- 修改本地时间到 15 天后（或修改 pluginTrialExpiresAt）
- 尝试发送消息
- 验证显示过期弹窗
- 验证功能被禁用

**测试用例 6：Token 过期处理**
- 使用过期的 Token 调用 API
- 验证后端返回 401
- 验证前端显示过期弹窗


---

## 8. 总结

### 8.1 核心改动点

**后端（最小改动）：**
1. `GET /system/user/getInfo` 接口新增 2 个字段
2. JWT Token 根据用户类型设置不同过期时间

**前端（主要改动）：**
1. 修改 UserInfo 接口定义
2. 修改 onTokenReceived() 添加判断逻辑
3. 新增 3 个文件（欢迎面板、过期面板、过期检测服务）
4. 在消息发送前添加过期检查

### 8.2 关键判断逻辑

```typescript
// 登录后判断
if (userInfo.isPluginTrial === true) {
  showWelcomePanel();  // 显示欢迎弹窗
} else {
  showInvitationModal();  // 显示邀请码弹窗
}

// 使用前判断
if (Date.now() >= userInfo.pluginTrialExpiresAt) {
  showExpiredPanel();  // 显示过期弹窗
  return;  // 禁止使用
}
```


### 8.3 必须注意的问题

1. **时间同步问题** - 用户本地时间不准确导致误判，建议调用后端验证
2. **isPluginTrial 严格判断** - 必须使用 `=== true` 判断
3. **欢迎弹窗重复显示** - 使用 globalState 标记避免重复
4. **Token 过期处理** - 在 apiClient 中统一处理 401 响应

### 8.4 实现优先级

**P0（必须实现）：**
1. 后端接口新增字段
2. 前端 UserInfo 接口修改
3. 前端 onTokenReceived() 判断逻辑
4. 过期检测逻辑

**P1（重要）：**
1. 欢迎弹窗
2. 过期提醒弹窗

**P2（优化）：**
1. 后端验证过期（避免时间同步问题）
2. 更友好的过期提示

---

**文档完成！** 基于你现有的代码架构，这个方案改动最小，实现最简单。

