Files
IC-Coder-Plugin/docs/invitation-code-design.md
Roe-xin 032dd1b215 feat: 实现邀请码验证功能
## 功能概述
   - 用户首次使用需验证邀请码才能发起对话
   - 在输入框聚焦和点击示例时触发验证检查
   - 使用弹窗形式展示邀请码输入界面,包含企业端用户提示和微信二维码

   ## 主要变更

   ### 新增文件
   - `services/invitationService.ts`: 邀请码验证服务,处理验证逻辑和状态管理
   - `views/invitationModal.ts`: 邀请码验证弹窗组件(HTML/CSS/JS)
   - `docs/invitation-code-design.md`: 邀请码功能设计文档

   ### 修改文件
   - `extension.ts`: 添加更换邀请码命令,退出登录时清除验证状态
   - `panels/ICHelperPanel.ts`: 添加邀请码验证状态检查和验证消息处理
   - `services/apiClient.ts`: 添加邀请码验证接口调用
   - `types/api.ts`: 添加邀请码相关类型定义
   - `views/inputArea.ts`: 输入框聚焦时触发邀请码验证检查
   - `views/exampleShowcase.ts`: 点击示例时先检查邀请码验证状态
   - `views/webviewContent.ts`: 集成邀请码弹窗到主界面

   ## 技术实现
   - 验证状态保存在 ExtensionContext.globalState 中
   - 使用后端接口 POST /api/invitation/verify 进行验证
   - 弹窗样式适配 VS Code 主题
   - 支持回车键提交验证
2026-01-27 14:40:31 +08:00

19 KiB
Raw Blame History

邀请码验证功能设计方案

一、整体流程

用户首次使用 → 检查邀请码状态 → 未验证则弹窗输入 → 后端验证 → 验证通过后可正常对话

二、前端设计

2.1 邀请码状态管理

ExtensionContext.globalState 中存储邀请码验证状态:

// 存储结构
{
  "invitationCodeVerified": true,
  "invitationCode": "INVITE2024ABC",
  "verifiedTime": "2024-01-20T10:30:00"
}

2.2 UI 交互流程

弹窗输入邀请码

使用 vscode.window.showInputBox 实现:

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 - 邀请码服务

/**
 * 邀请码验证服务
 */
export class InvitationService {
  /**
   * 检查用户是否已验证邀请码
   */
  static async isVerified(context: vscode.ExtensionContext): Promise<boolean>

  /**
   * 验证邀请码
   */
  static async verifyCode(code: string): Promise<boolean>

  /**
   * 保存验证状态
   */
  static async saveVerificationStatus(
    context: vscode.ExtensionContext,
    code: string
  ): Promise<void>

  /**
   * 清除验证状态(用于退出登录)
   */
  static async clearVerificationStatus(
    context: vscode.ExtensionContext
  ): Promise<void>

  /**
   * 显示邀请码输入弹窗
   */
  static async showInputDialog(): Promise<string | undefined>
}

修改文件

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 中添加:

// ============== 邀请码验证 ==============

/**
 * 邀请码验证请求
 * 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)

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)

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

请求体

{
  "code": "INVITE2024ABC"
}

响应示例

成功:

{
  "code": 200,
  "msg": "验证成功",
  "data": {
    "verified": true
  }
}

失败:

{
  "code": 400,
  "msg": "邀请码无效或已过期"
}
{
  "code": 400,
  "msg": "邀请码使用次数已达上限"
}
{
  "code": 400,
  "msg": "您已验证过邀请码,无需重复验证"
}

3.2.2 查询验证状态

接口地址GET /api/invitation/status

请求头

Authorization: Bearer {token}

响应示例

已验证:

{
  "code": 200,
  "msg": "success",
  "data": {
    "verified": true,
    "invitationCode": "INVITE2024ABC",
    "verifiedTime": "2024-01-20T10:30:00"
  }
}

未验证:

{
  "code": 200,
  "msg": "success",
  "data": {
    "verified": false
  }
}

3.2.3 管理接口(管理员使用)

生成邀请码POST /api/admin/invitation/generate

请求体:

{
  "count": 10,
  "maxUses": 1,
  "expireTime": "2024-12-31T23:59:59",
  "remark": "2024年1月批次"
}

响应:

{
  "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 后端验证逻辑

验证流程

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 权限拦截

在对话接口中添加邀请码验证拦截:

@PostMapping("/dialog/stream")
public SseEmitter dialog(@RequestBody DialogRequest request) {
    // 获取当前用户ID
    Long userId = SecurityUtils.getUserId();

    // 检查用户是否已验证邀请码
    if (!invitationService.isUserVerified(userId)) {
        throw new BusinessException("请先验证邀请码后再使用对话功能");
    }

    // 继续处理对话请求
    // ...
}

或者使用拦截器统一处理:

@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 首次使用引导

在用户首次打开聊天面板时,如果未验证邀请码,显示友好的引导信息:

// 在 ICHelperPanel.ts 中
if (!await InvitationService.isVerified(context)) {
  panel.webview.postMessage({
    command: 'showInvitationGuide',
    message: '欢迎使用 IC Coder请先输入邀请码以开始使用。'
  });
}

4.2 状态持久化

验证状态保存在 globalState 中,避免重复验证:

// 保存验证状态
await context.globalState.update('invitationCodeVerified', true);
await context.globalState.update('invitationCode', code);
await context.globalState.update('verifiedTime', new Date().toISOString());

4.3 错误提示优化

根据不同的错误类型,提供清晰的提示信息:

错误类型 提示信息
邀请码不存在 "邀请码不存在,请检查后重新输入"
邀请码已过期 "邀请码已过期,请联系管理员获取新的邀请码"
使用次数已达上限 "该邀请码使用次数已达上限,请使用其他邀请码"
已验证过 "您已验证过邀请码,无需重复验证"
网络错误 "网络连接失败,请检查网络后重试"

4.4 支持重新验证

提供命令允许用户更换邀请码:

// 在 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 中显示状态(可选)

在聊天界面顶部显示邀请码验证状态:

<!-- 已验证 -->
<div class="invitation-status verified">
  <span class="icon"></span>
  <span>邀请码已验证</span>
</div>

<!-- 未验证 -->
<div class="invitation-status unverified">
  <span class="icon">!</span>
  <span>请先验证邀请码</span>
  <button onclick="verifyInvitationCode()">立即验证</button>
</div>

五、安全考虑

5.1 邀请码生成规则

使用安全的随机算法生成邀请码:

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 防暴力破解

限制验证频率,添加验证失败次数限制:

// 使用 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 绑定,退出登录时清除:

// 在退出登录时清除验证状态
vscode.commands.registerCommand('ic-coder.logout', async () => {
  // 清除 session
  await clearSession();

  // 清除邀请码验证状态
  await InvitationService.clearVerificationStatus(context);

  vscode.window.showInformationMessage('已退出登录');
});

5.4 日志记录

记录所有验证尝试,便于审计和分析:

@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