# 邀请码验证功能设计方案 ## 一、整体流程 ``` 用户首次使用 → 检查邀请码状态 → 未验证则弹窗输入 → 后端验证 → 验证通过后可正常对话 ``` ## 二、前端设计 ### 2.1 邀请码状态管理 在 `ExtensionContext.globalState` 中存储邀请码验证状态: ```typescript // 存储结构 { "invitationCodeVerified": true, "invitationCode": "INVITE2024ABC", "verifiedTime": "2024-01-20T10:30:00" } ``` ### 2.2 UI 交互流程 #### 弹窗输入邀请码 使用 `vscode.window.showInputBox` 实现: ```typescript const invitationCode = await vscode.window.showInputBox({ prompt: '请输入邀请码以继续使用 IC Coder', placeHolder: '例如:INVITE2024ABC', ignoreFocusOut: true, validateInput: (value) => { if (!value || value.trim().length === 0) { return '邀请码不能为空'; } if (value.length < 6) { return '邀请码格式不正确'; } return null; } }); ``` #### 验证结果提示 - 成功:`vscode.window.showInformationMessage('邀请码验证成功!')` - 失败:`vscode.window.showErrorMessage('邀请码无效或已过期,请重新输入')` ### 2.3 验证时机 在以下场景触发邀请码验证: 1. **用户首次发送消息时**(在 `handleUserMessage` 中检查) 2. **用户登录后**(在登录成功回调中检查) 3. **Token 过期重新登录后** ### 2.4 前端验证流程图 ``` 发送消息前检查 ├─ 检查是否已登录 │ └─ 未登录 → 提示登录 ├─ 检查邀请码是否已验证 │ ├─ 未验证 │ │ ├─ 弹窗输入邀请码 │ │ ├─ 调用后端验证接口 POST /api/invitation/verify │ │ ├─ 验证成功 │ │ │ ├─ 保存验证状态到 globalState │ │ │ └─ 继续发送消息 │ │ └─ 验证失败 │ │ ├─ 显示错误提示 │ │ └─ 阻止发送消息 │ └─ 已验证 → 继续发送消息 ``` ### 2.5 前端文件修改清单 #### 新增文件 **`src/services/invitationService.ts`** - 邀请码服务 ```typescript /** * 邀请码验证服务 */ export class InvitationService { /** * 检查用户是否已验证邀请码 */ static async isVerified(context: vscode.ExtensionContext): Promise /** * 验证邀请码 */ static async verifyCode(code: string): Promise /** * 保存验证状态 */ static async saveVerificationStatus( context: vscode.ExtensionContext, code: string ): Promise /** * 清除验证状态(用于退出登录) */ static async clearVerificationStatus( context: vscode.ExtensionContext ): Promise /** * 显示邀请码输入弹窗 */ static async showInputDialog(): Promise } ``` #### 修改文件 **`src/utils/messageHandler.ts`** - 在 `handleUserMessage` 函数开头添加邀请码验证检查 **`src/services/apiClient.ts`** - 添加 `verifyInvitationCode` 函数 - 添加 `checkInvitationStatus` 函数 **`src/types/api.ts`** - 添加邀请码相关类型定义 **`src/panels/ICHelperPanel.ts`** - 在面板创建时检查邀请码状态(可选) **`src/extension.ts`** - 在登录成功后检查邀请码状态 ### 2.6 类型定义 在 `src/types/api.ts` 中添加: ```typescript // ============== 邀请码验证 ============== /** * 邀请码验证请求 * POST /api/invitation/verify */ export interface InvitationVerifyRequest { /** 邀请码 */ code: string; } /** * 邀请码验证响应 */ export interface InvitationVerifyResponse { /** 响应代码 */ code: number; /** 响应消息 */ msg: string; /** 验证结果数据 */ data?: { /** 是否验证成功 */ verified: boolean; }; } /** * 邀请码状态响应 * GET /api/invitation/status */ export interface InvitationStatusResponse { /** 响应代码 */ code: number; /** 响应消息 */ msg?: string; /** 状态数据 */ data?: { /** 是否已验证 */ verified: boolean; /** 使用的邀请码 */ invitationCode?: string; /** 验证时间 */ verifiedTime?: string; }; } ``` ## 三、后端设计 ### 3.1 数据库设计 #### 邀请码表 (invitation_codes) ```sql CREATE TABLE invitation_codes ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', code VARCHAR(32) UNIQUE NOT NULL COMMENT '邀请码', max_uses INT DEFAULT 1 COMMENT '最大使用次数,-1表示无限制', used_count INT DEFAULT 0 COMMENT '已使用次数', expire_time DATETIME COMMENT '过期时间,NULL表示永不过期', created_by BIGINT COMMENT '创建者用户ID', created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', status TINYINT DEFAULT 1 COMMENT '状态:1-有效,0-禁用', remark VARCHAR(500) COMMENT '备注', INDEX idx_code (code), INDEX idx_status (status), INDEX idx_expire_time (expire_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请码表'; ``` #### 用户邀请码关联表 (user_invitation) ```sql CREATE TABLE user_invitation ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', user_id BIGINT NOT NULL COMMENT '用户ID', invitation_code VARCHAR(32) NOT NULL COMMENT '使用的邀请码', verified_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '验证时间', ip_address VARCHAR(50) COMMENT '验证时的IP地址', UNIQUE KEY uk_user_id (user_id), INDEX idx_invitation_code (invitation_code), INDEX idx_verified_time (verified_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请码关联表'; ``` ### 3.2 API 接口设计 #### 3.2.1 验证邀请码 **接口地址**:`POST /api/invitation/verify` **请求头**: ``` Authorization: Bearer {token} Content-Type: application/json ``` **请求体**: ```json { "code": "INVITE2024ABC" } ``` **响应示例**: 成功: ```json { "code": 200, "msg": "验证成功", "data": { "verified": true } } ``` 失败: ```json { "code": 400, "msg": "邀请码无效或已过期" } ``` ```json { "code": 400, "msg": "邀请码使用次数已达上限" } ``` ```json { "code": 400, "msg": "您已验证过邀请码,无需重复验证" } ``` #### 3.2.2 查询验证状态 **接口地址**:`GET /api/invitation/status` **请求头**: ``` Authorization: Bearer {token} ``` **响应示例**: 已验证: ```json { "code": 200, "msg": "success", "data": { "verified": true, "invitationCode": "INVITE2024ABC", "verifiedTime": "2024-01-20T10:30:00" } } ``` 未验证: ```json { "code": 200, "msg": "success", "data": { "verified": false } } ``` #### 3.2.3 管理接口(管理员使用) **生成邀请码**:`POST /api/admin/invitation/generate` 请求体: ```json { "count": 10, "maxUses": 1, "expireTime": "2024-12-31T23:59:59", "remark": "2024年1月批次" } ``` 响应: ```json { "code": 200, "msg": "生成成功", "data": { "codes": [ "INVITE2024001", "INVITE2024002", "..." ] } } ``` **查询邀请码列表**:`GET /api/admin/invitation/list` **禁用邀请码**:`PUT /api/admin/invitation/disable/{code}` **查询使用记录**:`GET /api/admin/invitation/usage/{code}` ### 3.3 后端验证逻辑 #### 验证流程 ```java public boolean verifyInvitationCode(Long userId, String code) { // 1. 检查邀请码是否存在 InvitationCode invitationCode = invitationCodeMapper.selectByCode(code); if (invitationCode == null) { throw new BusinessException("邀请码不存在"); } // 2. 检查邀请码状态 if (invitationCode.getStatus() != 1) { throw new BusinessException("邀请码已被禁用"); } // 3. 检查是否过期 if (invitationCode.getExpireTime() != null && invitationCode.getExpireTime().before(new Date())) { throw new BusinessException("邀请码已过期"); } // 4. 检查使用次数 if (invitationCode.getMaxUses() != -1 && invitationCode.getUsedCount() >= invitationCode.getMaxUses()) { throw new BusinessException("邀请码使用次数已达上限"); } // 5. 检查用户是否已验证过 UserInvitation existing = userInvitationMapper.selectByUserId(userId); if (existing != null) { throw new BusinessException("您已验证过邀请码,无需重复验证"); } // 6. 创建用户验证记录 UserInvitation userInvitation = new UserInvitation(); userInvitation.setUserId(userId); userInvitation.setInvitationCode(code); userInvitation.setVerifiedTime(new Date()); userInvitationMapper.insert(userInvitation); // 7. 增加邀请码使用次数 invitationCodeMapper.incrementUsedCount(code); return true; } ``` ### 3.4 权限拦截 在对话接口中添加邀请码验证拦截: ```java @PostMapping("/dialog/stream") public SseEmitter dialog(@RequestBody DialogRequest request) { // 获取当前用户ID Long userId = SecurityUtils.getUserId(); // 检查用户是否已验证邀请码 if (!invitationService.isUserVerified(userId)) { throw new BusinessException("请先验证邀请码后再使用对话功能"); } // 继续处理对话请求 // ... } ``` 或者使用拦截器统一处理: ```java @Component public class InvitationInterceptor implements HandlerInterceptor { @Autowired private InvitationService invitationService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 获取当前用户ID Long userId = SecurityUtils.getUserId(); // 检查是否需要验证邀请码的接口 String uri = request.getRequestURI(); if (needsInvitationVerification(uri)) { if (!invitationService.isUserVerified(userId)) { throw new BusinessException("请先验证邀请码"); } } return true; } private boolean needsInvitationVerification(String uri) { // 需要验证邀请码的接口列表 return uri.startsWith("/api/dialog/") || uri.startsWith("/api/task/"); } } ``` ### 3.5 后端文件清单 #### 实体类 - `com.iccoder.entity.InvitationCode` - 邀请码实体 - `com.iccoder.entity.UserInvitation` - 用户邀请码关联实体 #### Mapper - `com.iccoder.mapper.InvitationCodeMapper` - 邀请码数据访问 - `com.iccoder.mapper.UserInvitationMapper` - 用户邀请码关联数据访问 #### Service - `com.iccoder.service.InvitationService` - 邀请码业务逻辑 - `com.iccoder.service.impl.InvitationServiceImpl` - 实现类 #### Controller - `com.iccoder.controller.InvitationController` - 邀请码接口 - `com.iccoder.controller.admin.InvitationAdminController` - 管理接口 #### 拦截器 - `com.iccoder.interceptor.InvitationInterceptor` - 邀请码验证拦截器 ## 四、用户体验优化 ### 4.1 首次使用引导 在用户首次打开聊天面板时,如果未验证邀请码,显示友好的引导信息: ```typescript // 在 ICHelperPanel.ts 中 if (!await InvitationService.isVerified(context)) { panel.webview.postMessage({ command: 'showInvitationGuide', message: '欢迎使用 IC Coder!请先输入邀请码以开始使用。' }); } ``` ### 4.2 状态持久化 验证状态保存在 `globalState` 中,避免重复验证: ```typescript // 保存验证状态 await context.globalState.update('invitationCodeVerified', true); await context.globalState.update('invitationCode', code); await context.globalState.update('verifiedTime', new Date().toISOString()); ``` ### 4.3 错误提示优化 根据不同的错误类型,提供清晰的提示信息: | 错误类型 | 提示信息 | |---------|---------| | 邀请码不存在 | "邀请码不存在,请检查后重新输入" | | 邀请码已过期 | "邀请码已过期,请联系管理员获取新的邀请码" | | 使用次数已达上限 | "该邀请码使用次数已达上限,请使用其他邀请码" | | 已验证过 | "您已验证过邀请码,无需重复验证" | | 网络错误 | "网络连接失败,请检查网络后重试" | ### 4.4 支持重新验证 提供命令允许用户更换邀请码: ```typescript // 在 extension.ts 中注册命令 context.subscriptions.push( vscode.commands.registerCommand('ic-coder.changeInvitationCode', async () => { const confirm = await vscode.window.showWarningMessage( '确定要更换邀请码吗?', '确定', '取消' ); if (confirm === '确定') { await InvitationService.clearVerificationStatus(context); vscode.window.showInformationMessage('已清除邀请码,请重新验证'); } }) ); ``` ### 4.5 Webview 中显示状态(可选) 在聊天界面顶部显示邀请码验证状态: ```html
邀请码已验证
! 请先验证邀请码
``` ## 五、安全考虑 ### 5.1 邀请码生成规则 使用安全的随机算法生成邀请码: ```java public String generateInvitationCode() { // 使用 UUID + 时间戳 + 随机数 String uuid = UUID.randomUUID().toString().replace("-", ""); String timestamp = String.valueOf(System.currentTimeMillis()); String random = RandomStringUtils.randomAlphanumeric(6); // 组合并取前16位 String combined = uuid + timestamp + random; String code = DigestUtils.sha256Hex(combined).substring(0, 16).toUpperCase(); return "IC" + code; // 添加前缀,例如:IC3F2A9B1C4D5E6F } ``` ### 5.2 防暴力破解 限制验证频率,添加验证失败次数限制: ```java // 使用 Redis 记录验证失败次数 String key = "invitation:fail:" + userId; Integer failCount = redisTemplate.opsForValue().get(key); if (failCount != null && failCount >= 5) { throw new BusinessException("验证失败次数过多,请1小时后再试"); } // 验证失败时增加计数 if (!verifySuccess) { redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, 1, TimeUnit.HOURS); } ``` ### 5.3 Token 绑定 邀请码验证状态与用户 Token 绑定,退出登录时清除: ```typescript // 在退出登录时清除验证状态 vscode.commands.registerCommand('ic-coder.logout', async () => { // 清除 session await clearSession(); // 清除邀请码验证状态 await InvitationService.clearVerificationStatus(context); vscode.window.showInformationMessage('已退出登录'); }); ``` ### 5.4 日志记录 记录所有验证尝试,便于审计和分析: ```java @Slf4j public class InvitationServiceImpl implements InvitationService { @Override public boolean verifyInvitationCode(Long userId, String code) { log.info("用户 {} 尝试验证邀请码: {}", userId, code); try { // 验证逻辑 // ... log.info("用户 {} 验证邀请码成功: {}", userId, code); return true; } catch (Exception e) { log.warn("用户 {} 验证邀请码失败: {}, 原因: {}", userId, code, e.getMessage()); throw e; } } } ``` ### 5.5 敏感信息保护 - 邀请码在数据库中可以考虑加密存储(可选) - API 响应中不暴露邀请码的详细信息(如剩余次数) - 前端不缓存邀请码明文,只保存验证状态 ## 六、实施步骤 ### 阶段一:后端开发(优先) 1. 创建数据库表 2. 实现邀请码生成和管理功能 3. 实现验证接口 4. 添加权限拦截 5. 测试接口功能 ### 阶段二:前端开发 1. 添加类型定义 2. 实现 `InvitationService` 3. 修改 `apiClient.ts` 添加接口调用 4. 修改 `messageHandler.ts` 添加验证检查 5. 测试完整流程 ### 阶段三:联调测试 1. 前后端联调 2. 测试各种异常场景 3. 优化用户体验 4. 性能测试 ### 阶段四:上线部署 1. 生成初始邀请码 2. 更新用户文档 3. 灰度发布 4. 监控运行状态 ## 七、测试用例 ### 7.1 正常流程测试 | 测试场景 | 预期结果 | |---------|---------| | 首次使用,输入有效邀请码 | 验证成功,可以正常对话 | | 已验证用户再次打开面板 | 无需重复验证,直接使用 | | 退出登录后重新登录 | 需要重新验证邀请码 | ### 7.2 异常场景测试 | 测试场景 | 预期结果 | |---------|---------| | 输入不存在的邀请码 | 提示"邀请码不存在" | | 输入已过期的邀请码 | 提示"邀请码已过期" | | 输入使用次数已满的邀请码 | 提示"使用次数已达上限" | | 已验证用户尝试再次验证 | 提示"已验证过,无需重复验证" | | 网络断开时验证 | 提示"网络连接失败" | | 连续输入错误邀请码5次 | 提示"验证失败次数过多,请稍后再试" | ### 7.3 边界条件测试 | 测试场景 | 预期结果 | |---------|---------| | 邀请码为空 | 前端验证拦截,提示"邀请码不能为空" | | 邀请码长度不足 | 前端验证拦截,提示"邀请码格式不正确" | | 邀请码包含特殊字符 | 后端验证失败,提示"邀请码不存在" | | 同一邀请码多人同时使用 | 使用数据库锁,确保不超过最大次数 | ## 八、FAQ ### Q1: 用户忘记邀请码怎么办? A: 邀请码验证成功后,用户无需记住邀请码。如果需要查看,可以在设置中显示已验证的邀请码。 ### Q2: 邀请码可以重复使用吗? A: 取决于邀请码的 `maxUses` 设置。可以设置为 1(一次性)、N(限定次数)或 -1(无限制)。 ### Q3: 如何批量生成邀请码? A: 使用管理接口 `POST /api/admin/invitation/generate`,指定生成数量即可。 ### Q4: 邀请码验证失败会影响登录吗? A: 不会。邀请码验证是独立的,只影响对话功能的使用,不影响登录。 ### Q5: 可以为不同用户群体设置不同的邀请码吗? A: 可以。通过 `remark` 字段标记不同批次的邀请码,便于管理和统计。 ## 九、后续优化方向 1. **邀请码分级**:不同等级的邀请码对应不同的权限(如对话次数、模型选择等) 2. **邀请奖励**:邀请他人使用可获得积分或额外权限 3. **邀请统计**:统计每个邀请码的使用情况和用户活跃度 4. **自动过期**:根据使用情况自动延长或缩短邀请码有效期 5. **白名单机制**:特定用户可以免邀请码使用 --- **文档版本**:v1.0 **最后更新**:2026-01-27 **维护者**:IC Coder Team