784 lines
20 KiB
Markdown
784 lines
20 KiB
Markdown
# 插件试用用户功能实现方案
|
||
|
||
## 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. 更友好的过期提示
|
||
|
||
---
|
||
|
||
**文档完成!** 基于你现有的代码架构,这个方案改动最小,实现最简单。
|
||
|