- 修复 userService 中 null 值未正确赋值的问题 - 优化欢迎弹窗判断逻辑:null=长期有效,undefined=无效 - 添加测试命令 resetWelcomeModal 用于清除弹窗标记
439 lines
14 KiB
TypeScript
439 lines
14 KiB
TypeScript
/**
|
||
* 用户服务
|
||
* 管理用户信息和认证相关的 API 调用
|
||
*/
|
||
import * as https from 'https';
|
||
import * as http from 'http';
|
||
import { URL } from 'url';
|
||
import * as vscode from 'vscode';
|
||
import { getStrangeLoopApiUrl, getConfig } from '../config/settings';
|
||
import type { UserInfoResponse, MembershipResponse, MultiMembershipVO, MembershipItemVO } from '../types/api';
|
||
import { fetchBalanceWithToken, getCachedBalance } from './creditsService';
|
||
|
||
/**
|
||
* HTTP 请求选项
|
||
*/
|
||
interface RequestOptions {
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||
headers?: Record<string, string>;
|
||
body?: unknown;
|
||
timeout?: number;
|
||
token?: string;
|
||
}
|
||
|
||
/**
|
||
* 发送 HTTP 请求(带 token)
|
||
*/
|
||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||
const url = new URL(getStrangeLoopApiUrl(path));
|
||
const { timeout } = getConfig();
|
||
|
||
const isHttps = url.protocol === 'https:';
|
||
const httpModule = isHttps ? https : http;
|
||
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
...options.headers
|
||
};
|
||
|
||
// 如果有 token,添加到请求头
|
||
if (options.token) {
|
||
headers['Authorization'] = `Bearer ${options.token}`;
|
||
}
|
||
|
||
const requestOptions: http.RequestOptions = {
|
||
hostname: url.hostname,
|
||
port: url.port || (isHttps ? 443 : 80),
|
||
path: url.pathname + url.search,
|
||
method: options.method,
|
||
headers,
|
||
timeout: options.timeout || timeout
|
||
};
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const req = httpModule.request(requestOptions, (res) => {
|
||
let data = '';
|
||
|
||
res.on('data', (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
res.on('end', () => {
|
||
console.log(`[HTTP] 响应状态码: ${res.statusCode}`);
|
||
console.log(`[HTTP] 响应内容: ${data}`);
|
||
|
||
try {
|
||
const json = JSON.parse(data);
|
||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||
resolve(json as T);
|
||
} else {
|
||
reject(new Error(json.error || json.message || json.msg || `HTTP ${res.statusCode}`));
|
||
}
|
||
} catch (e) {
|
||
// 如果不是 JSON,直接返回原始内容
|
||
reject(new Error(`解析响应失败 (${res.statusCode}): ${data}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', (error) => {
|
||
reject(error);
|
||
});
|
||
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
reject(new Error('请求超时'));
|
||
});
|
||
|
||
if (options.body) {
|
||
req.write(JSON.stringify(options.body));
|
||
}
|
||
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 用户信息数据结构(实际返回的数据)
|
||
*/
|
||
export 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 余额
|
||
credits?: number;
|
||
// 插件试用用户标识(从 JWT token 中提取)
|
||
isPluginTrial?: boolean;
|
||
// 试用到期时间(毫秒时间戳)
|
||
pluginTrialExpiresAt?: number;
|
||
}
|
||
|
||
/**
|
||
* 获取用户信息
|
||
* GET /system/user/getInfo
|
||
*/
|
||
export async function getUserInfo(token: string): Promise<UserInfo | null> {
|
||
const apiPath = '/system/user/getInfo';
|
||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||
console.log('[UserService] 获取用户信息');
|
||
console.log('[UserService] 请求地址:', fullUrl);
|
||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||
|
||
try {
|
||
const response = await request<UserInfoResponse>(apiPath, {
|
||
method: 'GET',
|
||
token
|
||
});
|
||
|
||
// 处理响应数据 - 检查 code 是否为 200
|
||
if (response.code === 200 && response.user) {
|
||
const user = response.user;
|
||
const userInfo: UserInfo = {
|
||
userId: String(user.userId),
|
||
username: user.userName,
|
||
nickname: user.nickName,
|
||
email: user.email,
|
||
phonenumber: user.phonenumber,
|
||
avatar: user.avatar,
|
||
roles: response.roles,
|
||
permissions: response.permissions,
|
||
createTime: user.createTime,
|
||
loginDate: user.loginDate
|
||
};
|
||
|
||
// 从接口响应中获取企业试用标识
|
||
if (response.isPluginTrial === true) {
|
||
userInfo.isPluginTrial = true;
|
||
console.log('[UserService] 从 getInfo 接口获取到 isPluginTrial: true');
|
||
}
|
||
|
||
// 获取试用到期时间(null 表示长期有效)
|
||
if (response.enterpriseTrialExpires !== undefined) {
|
||
userInfo.pluginTrialExpiresAt = response.enterpriseTrialExpires;
|
||
if (response.enterpriseTrialExpires === null) {
|
||
console.log('[UserService] 试用长期有效');
|
||
} else {
|
||
console.log('[UserService] 试用到期时间:', new Date(response.enterpriseTrialExpires).toLocaleString());
|
||
}
|
||
}
|
||
|
||
return userInfo;
|
||
}
|
||
|
||
console.error('[UserService] 获取用户信息失败:', response);
|
||
return null;
|
||
} catch (error) {
|
||
console.error('[UserService] 请求失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户会员信息
|
||
* GET /strangeloop/api/membership/current
|
||
*/
|
||
export async function getMembershipInfo(token: string): Promise<MultiMembershipVO | null> {
|
||
const apiPath = '/strangeloop/api/membership/current';
|
||
const fullUrl = getStrangeLoopApiUrl(apiPath);
|
||
console.log('[UserService] 获取会员信息');
|
||
console.log('[UserService] 请求地址:', fullUrl);
|
||
console.log('[UserService] Token:', token ? '已提供' : '未提供');
|
||
|
||
try {
|
||
const response = await request<MembershipResponse>(apiPath, {
|
||
method: 'GET',
|
||
token
|
||
});
|
||
|
||
// 处理响应数据 - 检查 code 是否为 200
|
||
if (response.code === 200 && response.data) {
|
||
console.log('[UserService] 会员信息获取成功:', response.data);
|
||
return response.data;
|
||
}
|
||
|
||
console.error('[UserService] 获取会员信息失败:', response);
|
||
return null;
|
||
} catch (error) {
|
||
console.error('[UserService] 请求会员信息失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 会员等级映射
|
||
*/
|
||
const TIER_LEVEL_MAP: Record<string, number> = {
|
||
'BASIC': 1,
|
||
'TRIAL': 2,
|
||
'ADVANCED': 3,
|
||
'PROFESSIONAL': 4
|
||
};
|
||
|
||
/**
|
||
* 获取最高等级的会员信息
|
||
*/
|
||
function getHighestTierMembership(allMemberships?: MembershipItemVO[]): MembershipItemVO | null {
|
||
if (!allMemberships || allMemberships.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// 按等级排序,获取最高等级
|
||
return allMemberships.reduce((highest, current) => {
|
||
const currentLevel = TIER_LEVEL_MAP[current.tierCode] || 0;
|
||
const highestLevel = TIER_LEVEL_MAP[highest.tierCode] || 0;
|
||
return currentLevel > highestLevel ? current : highest;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 当获取到 token 时自动调用此函数
|
||
* 用于在登录成功后立即获取用户信息
|
||
*/
|
||
export async function onTokenReceived(token: string): Promise<UserInfo | null> {
|
||
try {
|
||
console.log('[UserService] Token 已获取,正在获取用户信息、会员信息和余额...');
|
||
|
||
// 并行获取用户信息、会员信息和余额
|
||
const [userInfo, membershipInfo, credits] = await Promise.all([
|
||
getUserInfo(token),
|
||
getMembershipInfo(token),
|
||
fetchBalanceWithToken(token)
|
||
]);
|
||
|
||
if (!userInfo) {
|
||
console.warn('[UserService] 未能获取到用户信息');
|
||
return null;
|
||
}
|
||
|
||
// 添加 Credits 余额到用户信息
|
||
console.log('[UserService] 获取到的 Credits 余额:', credits);
|
||
if (credits !== null) {
|
||
userInfo.credits = credits;
|
||
console.log('[UserService] Credits 已添加到用户信息');
|
||
} else {
|
||
console.warn('[UserService] Credits 余额为 null,未添加到用户信息');
|
||
}
|
||
|
||
// 打印用户信息到控制台
|
||
console.log('='.repeat(60));
|
||
console.log('用户信息详情:');
|
||
console.log('='.repeat(60));
|
||
console.log(`用户ID: ${userInfo.userId}`);
|
||
console.log(`用户名: ${userInfo.username}`);
|
||
console.log(`昵称: ${userInfo.nickname}`);
|
||
if (userInfo.email) {
|
||
console.log(`邮箱: ${userInfo.email}`);
|
||
}
|
||
if (userInfo.phonenumber) {
|
||
console.log(`手机号: ${userInfo.phonenumber}`);
|
||
}
|
||
if (userInfo.avatar) {
|
||
console.log(`头像: ${userInfo.avatar}`);
|
||
}
|
||
if (userInfo.roles && userInfo.roles.length > 0) {
|
||
console.log(`角色: ${userInfo.roles.join(', ')}`);
|
||
}
|
||
if (userInfo.permissions && userInfo.permissions.length > 0) {
|
||
console.log(`权限: ${userInfo.permissions.join(', ')}`);
|
||
}
|
||
if (userInfo.createTime) {
|
||
console.log(`创建时间: ${userInfo.createTime}`);
|
||
}
|
||
if (userInfo.loginDate) {
|
||
console.log(`最后登录: ${userInfo.loginDate}`);
|
||
}
|
||
|
||
// 打印会员信息 - 从 allMemberships 中获取最高等级
|
||
if (membershipInfo && membershipInfo.allMemberships) {
|
||
const highestTier = getHighestTierMembership(membershipInfo.allMemberships);
|
||
|
||
if (highestTier) {
|
||
console.log('');
|
||
console.log('会员信息:');
|
||
console.log(`会员等级: ${highestTier.tierName} (${highestTier.tierCode})`);
|
||
console.log(`等级层级: ${highestTier.tierLevel}`);
|
||
console.log(`剩余天数: ${highestTier.remainingDays === -1 ? '永久' : highestTier.remainingDays + '天'}`);
|
||
console.log(`月度积分: ${highestTier.monthlyCredits}`);
|
||
|
||
// 将最高等级会员信息合并到用户信息中
|
||
userInfo.membership = {
|
||
tierCode: highestTier.tierCode,
|
||
tierName: highestTier.tierName,
|
||
tierLevel: highestTier.tierLevel,
|
||
remainingDays: highestTier.remainingDays,
|
||
monthlyCredits: highestTier.monthlyCredits
|
||
};
|
||
}
|
||
}
|
||
|
||
// 打印 Credits 余额
|
||
console.log('');
|
||
console.log('资源点余额:');
|
||
if (userInfo.credits !== undefined) {
|
||
console.log(`当前余额: ${userInfo.credits} Credits`);
|
||
} else {
|
||
console.log('当前余额: 未获取到余额信息');
|
||
}
|
||
|
||
console.log('='.repeat(60));
|
||
|
||
// 保存到持久化存储
|
||
await saveUserInfo(userInfo);
|
||
|
||
// 判断是否是插件试用用户
|
||
console.log('[UserService] 检查用户类型,isPluginTrial:', userInfo.isPluginTrial);
|
||
console.log('[UserService] extensionContext 是否存在:', !!extensionContext);
|
||
|
||
if (userInfo.isPluginTrial === true && userInfo.pluginTrialExpiresAt !== null && userInfo.pluginTrialExpiresAt !== undefined) {
|
||
// 检查是否过期
|
||
const now = Date.now();
|
||
const isExpired = now >= userInfo.pluginTrialExpiresAt;
|
||
console.log('[UserService] 试用到期时间:', new Date(userInfo.pluginTrialExpiresAt).toLocaleString());
|
||
console.log('[UserService] 当前时间:', new Date(now).toLocaleString());
|
||
console.log('[UserService] 是否过期:', isExpired);
|
||
|
||
if (isExpired) {
|
||
// 已过期:显示邀请码弹窗
|
||
console.log('[UserService] 试用已过期,将显示邀请码弹窗');
|
||
} else {
|
||
// 未过期:显示欢迎弹窗
|
||
const hasWelcomed = extensionContext?.globalState.get('pluginTrialWelcomed');
|
||
console.log('[UserService] 是否已显示过欢迎弹窗:', hasWelcomed);
|
||
|
||
if (!hasWelcomed && extensionContext) {
|
||
await extensionContext.globalState.update('showWelcomeModal', true);
|
||
await extensionContext.globalState.update('pluginTrialWelcomed', true);
|
||
console.log('[UserService] ✅ 已设置欢迎弹窗标记 showWelcomeModal=true');
|
||
|
||
const checkMark = extensionContext.globalState.get('showWelcomeModal');
|
||
console.log('[UserService] 验证标记:', checkMark);
|
||
} else if (!extensionContext) {
|
||
console.error('[UserService] ❌ extensionContext 为 null,无法设置标记');
|
||
} else {
|
||
console.log('[UserService] 已经显示过欢迎弹窗,跳过');
|
||
}
|
||
}
|
||
} else {
|
||
// isPluginTrial=false 或 enterpriseTrialExpires 为 null:显示邀请码弹窗
|
||
console.log('[UserService] 非试用用户或无过期时间,将显示邀请码弹窗');
|
||
}
|
||
|
||
return userInfo;
|
||
} catch (error) {
|
||
console.error('[UserService] 获取用户信息失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ============== 持久化存储 ==============
|
||
|
||
let extensionContext: vscode.ExtensionContext | null = null;
|
||
|
||
/**
|
||
* 初始化用户服务(设置 context)
|
||
*/
|
||
export function initUserService(context: vscode.ExtensionContext): void {
|
||
extensionContext = context;
|
||
}
|
||
|
||
/**
|
||
* 保存用户信息到持久化存储
|
||
*/
|
||
export async function saveUserInfo(userInfo: UserInfo): Promise<void> {
|
||
if (!extensionContext) {
|
||
console.warn('[UserService] ExtensionContext 未初始化');
|
||
return;
|
||
}
|
||
await extensionContext.globalState.update('icCoderUserInfo', userInfo);
|
||
console.log('[UserService] 用户信息已保存到持久化存储');
|
||
}
|
||
|
||
/**
|
||
* 从持久化存储获取用户信息
|
||
*/
|
||
export function getCachedUserInfo(): UserInfo | null {
|
||
if (!extensionContext) {
|
||
console.warn('[UserService] ExtensionContext 未初始化');
|
||
return null;
|
||
}
|
||
const userInfo = extensionContext.globalState.get<UserInfo>('icCoderUserInfo') || null;
|
||
|
||
// 从 creditsService 加载余额并合并到用户信息中
|
||
if (userInfo) {
|
||
const cachedCredits = getCachedBalance();
|
||
if (cachedCredits !== null) {
|
||
userInfo.credits = cachedCredits;
|
||
console.log('[UserService] 从 creditsService 加载余额:', cachedCredits);
|
||
}
|
||
}
|
||
|
||
return userInfo;
|
||
}
|
||
|
||
/**
|
||
* 清除持久化存储的用户信息
|
||
*/
|
||
export async function clearUserInfo(): Promise<void> {
|
||
if (!extensionContext) {
|
||
console.warn('[UserService] ExtensionContext 未初始化');
|
||
return;
|
||
}
|
||
await extensionContext.globalState.update('icCoderUserInfo', undefined);
|
||
console.log('[UserService] 用户信息已清除');
|
||
}
|