# 插件试用用户功能实现方案 ## 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 ` 欢迎使用 IC Coder

🎉 欢迎使用 IC Coder!

您已成功激活 15 天试用期,让我们开始探索 IC Coder 的强大功能吧!

📝 步骤 1:打开聊天面板

点击侧边栏的 IC Coder 图标,或使用命令面板搜索 "IC Coder: Open Chat"

💬 步骤 2:输入您的需求

描述您想要生成的 Verilog 代码或需要帮助的问题,AI 将为您提供专业的解决方案

🔬 步骤 3:运行仿真

使用 "生成 VCD" 命令运行 iverilog 仿真,并通过波形查看器查看仿真结果

`; } 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 `

⏰ 您的试用期已到期

感谢您使用 IC Coder!您的 15 天试用期已结束。

如需继续使用,请联系我们获取正式版本。

`; } } ``` ### 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 { 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 { // 显示过期弹窗 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 { 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. 更友好的过期提示 --- **文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。