feat:资源点使用实时更新

This commit is contained in:
Roe-xin
2026-02-25 10:14:00 +08:00
parent 0ea3afbe70
commit 1467ae8a89
4 changed files with 819 additions and 1 deletions

View File

@ -0,0 +1,783 @@
# 插件试用用户功能实现方案
## 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
- 尝试发送消息
- 验证显示过期弹窗
- 验证功能被禁用
**测试用例 6Token 过期处理**
- 使用过期的 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. 更友好的过期提示
---
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。