教育场景 精选推荐

在线考试防作弊技术方案完整指南

HTMLPAGE 团队
30 分钟阅读

深入探讨在线教育平台的考试防作弊技术方案,包括身份验证、行为监控、AI 监考、浏览器锁定、题库随机化等多层次防护策略的设计与实现。

#在线考试 #防作弊 #AI监考 #身份验证 #行为分析 #考试安全 #题库设计 #监控系统

在线考试防作弊技术方案完整指南

随着在线教育的普及,如何保证远程考试的公平性和有效性成为一个重要挑战。本文将系统介绍在线考试防作弊的多层次技术方案,从身份验证到行为监控,从题库设计到 AI 辅助监考,构建完整的考试安全体系。

考试防作弊体系架构

整体架构设计

// 在线考试防作弊系统架构
interface ExamSecurityArchitecture {
  layers: {
    // 第一层:身份验证
    identity: {
      methods: ['多因子认证', '人脸识别', '活体检测', '身份证OCR']
      timing: '考试开始前和考试中持续验证'
    }
    
    // 第二层:环境监控
    environment: {
      methods: ['浏览器锁定', '摄像头监控', '屏幕录制', '环境检测']
      timing: '考试全程'
    }
    
    // 第三层:行为分析
    behavior: {
      methods: ['答题行为分析', '切屏检测', '复制粘贴监控', '异常操作识别']
      timing: '考试全程'
    }
    
    // 第四层:内容安全
    content: {
      methods: ['题库随机化', '动态试卷', '时间限制', '答案加密']
      timing: '试卷生成和提交时'
    }
    
    // 第五层:AI 辅助
    aiAssistance: {
      methods: ['AI监考', '相似度检测', '异常模式识别', '作弊风险评估']
      timing: '实时分析和事后审计'
    }
  }
  
  // 风险评估等级
  riskLevels: {
    low: '普通练习/小测验,最低限度防护'
    medium: '单元测试/期中考试,标准防护'
    high: '期末考试/认证考试,严格防护'
    critical: '高利害考试/职业资格认证,最高级别防护'
  }
}

身份验证系统

1. 多因子身份认证

// 多因子身份认证系统
class MultiFactorAuthSystem {
  // 认证因子配置
  private factorConfig: AuthFactorConfig = {
    // 知识因子
    knowledge: {
      password: { required: true, strength: 'high' },
      securityQuestions: { required: false, count: 2 }
    },
    // 持有因子
    possession: {
      smsOtp: { required: false, expiry: 300 },
      emailOtp: { required: false, expiry: 300 },
      authenticatorApp: { required: false }
    },
    // 生物特征因子
    biometric: {
      faceRecognition: { required: true, threshold: 0.85 },
      livenessDetection: { required: true },
      fingerprint: { required: false }
    }
  }
  
  // 执行考试前身份验证
  async verifyExamIdentity(
    userId: string,
    examConfig: ExamSecurityConfig
  ): Promise<IdentityVerificationResult> {
    const results: FactorVerificationResult[] = []
    
    // 1. 验证账户凭证
    const accountVerified = await this.verifyAccount(userId)
    results.push(accountVerified)
    
    // 2. 人脸识别验证
    if (examConfig.requireFaceVerification) {
      const faceResult = await this.verifyFace(userId)
      results.push(faceResult)
    }
    
    // 3. 身份证验证
    if (examConfig.requireIdVerification) {
      const idResult = await this.verifyIdDocument(userId)
      results.push(idResult)
    }
    
    // 4. 活体检测
    if (examConfig.requireLivenessCheck) {
      const livenessResult = await this.performLivenessCheck(userId)
      results.push(livenessResult)
    }
    
    // 综合评估
    return this.evaluateVerificationResults(results, examConfig)
  }
  
  // 人脸识别验证
  private async verifyFace(userId: string): Promise<FactorVerificationResult> {
    // 获取用户注册的人脸特征
    const registeredFace = await this.getFaceEmbedding(userId)
    
    // 捕获当前人脸
    const currentFace = await this.captureFace()
    
    // 计算相似度
    const similarity = this.calculateFaceSimilarity(
      registeredFace.embedding,
      currentFace.embedding
    )
    
    // 判断是否通过
    const passed = similarity >= this.factorConfig.biometric.faceRecognition.threshold
    
    return {
      factor: 'face_recognition',
      passed,
      confidence: similarity,
      details: {
        similarity,
        threshold: this.factorConfig.biometric.faceRecognition.threshold,
        capturedAt: new Date()
      }
    }
  }
  
  // 活体检测
  private async performLivenessCheck(userId: string): Promise<FactorVerificationResult> {
    const challenges: LivenessChallenge[] = [
      { type: 'blink', instruction: '请眨眼' },
      { type: 'turn_left', instruction: '请向左转头' },
      { type: 'turn_right', instruction: '请向右转头' },
      { type: 'smile', instruction: '请微笑' }
    ]
    
    // 随机选择2-3个挑战
    const selectedChallenges = this.selectRandomChallenges(challenges, 2)
    
    const results: ChallengeResult[] = []
    for (const challenge of selectedChallenges) {
      const result = await this.executeLivenessChallenge(challenge)
      results.push(result)
      
      // 任一挑战失败则终止
      if (!result.passed) {
        return {
          factor: 'liveness_detection',
          passed: false,
          confidence: 0,
          details: { failedChallenge: challenge.type }
        }
      }
    }
    
    return {
      factor: 'liveness_detection',
      passed: true,
      confidence: results.reduce((sum, r) => sum + r.confidence, 0) / results.length,
      details: { completedChallenges: results }
    }
  }
  
  // 身份证OCR验证
  private async verifyIdDocument(userId: string): Promise<FactorVerificationResult> {
    // 捕获身份证图像
    const idImage = await this.captureIdDocument()
    
    // OCR 识别
    const ocrResult = await this.performIdOCR(idImage)
    
    // 获取用户注册信息
    const userInfo = await this.getUserRegistrationInfo(userId)
    
    // 验证信息匹配
    const nameMatch = this.fuzzyMatch(ocrResult.name, userInfo.name)
    const idNumberMatch = ocrResult.idNumber === userInfo.idNumber
    
    // 验证身份证照片与当前人脸
    const photoMatch = await this.compareIdPhotoWithFace(ocrResult.photo, userId)
    
    return {
      factor: 'id_document',
      passed: nameMatch && idNumberMatch && photoMatch > 0.8,
      confidence: (nameMatch ? 0.33 : 0) + (idNumberMatch ? 0.34 : 0) + (photoMatch * 0.33),
      details: {
        nameMatch,
        idNumberMatch,
        photoMatchScore: photoMatch
      }
    }
  }
}

2. 考试中持续身份验证

// 持续身份验证服务
class ContinuousAuthService {
  private verificationInterval: number = 60000 // 每分钟验证一次
  private faceMatchThreshold: number = 0.8
  
  // 启动持续验证
  async startContinuousVerification(
    examSessionId: string,
    userId: string
  ): Promise<void> {
    const session = await this.getExamSession(examSessionId)
    
    // 注册周期性验证任务
    this.scheduler.addTask({
      id: `continuous_auth_${examSessionId}`,
      interval: this.verificationInterval,
      task: async () => {
        await this.performPeriodicVerification(examSessionId, userId)
      }
    })
    
    // 注册随机验证任务
    this.scheduleRandomVerifications(examSessionId, userId, session.duration)
  }
  
  // 执行周期性验证
  private async performPeriodicVerification(
    examSessionId: string,
    userId: string
  ): Promise<void> {
    try {
      // 捕获当前人脸
      const currentFrame = await this.captureFrame()
      
      // 检测人脸
      const faceDetection = await this.detectFace(currentFrame)
      
      if (!faceDetection.detected) {
        // 人脸未检测到
        await this.recordAnomaly(examSessionId, {
          type: 'face_not_detected',
          severity: 'high',
          timestamp: new Date()
        })
        return
      }
      
      // 检测是否有多人
      if (faceDetection.faceCount > 1) {
        await this.recordAnomaly(examSessionId, {
          type: 'multiple_faces',
          severity: 'critical',
          faceCount: faceDetection.faceCount,
          timestamp: new Date()
        })
      }
      
      // 验证身份匹配
      const matchResult = await this.verifyFaceMatch(userId, faceDetection.embedding)
      
      if (matchResult.similarity < this.faceMatchThreshold) {
        await this.recordAnomaly(examSessionId, {
          type: 'face_mismatch',
          severity: 'critical',
          similarity: matchResult.similarity,
          timestamp: new Date()
        })
      }
      
      // 记录验证日志
      await this.logVerification(examSessionId, {
        type: 'periodic',
        passed: matchResult.similarity >= this.faceMatchThreshold,
        similarity: matchResult.similarity,
        timestamp: new Date()
      })
      
    } catch (error) {
      await this.handleVerificationError(examSessionId, error)
    }
  }
  
  // 随机触发验证
  private scheduleRandomVerifications(
    examSessionId: string,
    userId: string,
    examDuration: number
  ): void {
    // 计算随机验证次数
    const verificationCount = Math.floor(examDuration / (10 * 60 * 1000)) // 平均每10分钟一次
    
    // 生成随机时间点
    const verificationTimes = this.generateRandomTimes(examDuration, verificationCount)
    
    for (const time of verificationTimes) {
      setTimeout(async () => {
        await this.performRandomChallenge(examSessionId, userId)
      }, time)
    }
  }
  
  // 随机挑战验证
  private async performRandomChallenge(
    examSessionId: string,
    userId: string
  ): Promise<void> {
    const challenges = [
      { type: 'show_id', instruction: '请将身份证放在摄像头前' },
      { type: 'gesture', instruction: '请做出OK手势' },
      { type: 'voice', instruction: '请说出验证码' },
      { type: 'position', instruction: '请将脸部对准框内' }
    ]
    
    const challenge = this.selectRandomChallenge(challenges)
    
    // 通知前端显示挑战
    await this.notifyChallenge(examSessionId, challenge)
    
    // 等待用户响应
    const response = await this.waitForChallengeResponse(examSessionId, 30000)
    
    // 验证响应
    const passed = await this.verifyChallengeResponse(challenge, response)
    
    // 记录结果
    await this.recordChallengeResult(examSessionId, {
      challenge: challenge.type,
      passed,
      responseTime: response?.responseTime,
      timestamp: new Date()
    })
  }
}

环境监控系统

1. 浏览器锁定

// 安全浏览器锁定
class SecureBrowserLockdown {
  // 锁定配置
  private lockdownConfig: LockdownConfig = {
    // 禁止的操作
    blockedActions: {
      copyPaste: true,           // 禁止复制粘贴
      printScreen: true,         // 禁止截图
      contextMenu: true,         // 禁止右键菜单
      developerTools: true,      // 禁止开发者工具
      newTab: true,              // 禁止新标签页
      navigation: true,          // 禁止导航
      windowResize: false,       // 允许窗口调整
      fullscreenExit: true       // 禁止退出全屏
    },
    
    // 检测项目
    detections: {
      tabSwitch: true,           // 标签页切换检测
      windowBlur: true,          // 窗口失焦检测
      screenCapture: true,       // 截屏软件检测
      virtualMachine: true,      // 虚拟机检测
      remoteDesktop: true,       // 远程桌面检测
      multipleDisplays: true     // 多显示器检测
    }
  }
  
  // 初始化锁定
  async initializeLockdown(): Promise<void> {
    // 进入全屏模式
    await this.enterFullscreen()
    
    // 设置事件监听器
    this.setupEventListeners()
    
    // 启动环境检测
    await this.startEnvironmentDetection()
    
    // 禁用快捷键
    this.disableShortcuts()
    
    // 启动心跳检测
    this.startHeartbeat()
  }
  
  // 设置事件监听器
  private setupEventListeners(): void {
    // 禁止右键菜单
    document.addEventListener('contextmenu', (e) => {
      e.preventDefault()
      this.recordViolation('context_menu_attempt')
    })
    
    // 禁止复制粘贴
    document.addEventListener('copy', (e) => {
      e.preventDefault()
      this.recordViolation('copy_attempt')
    })
    
    document.addEventListener('paste', (e) => {
      e.preventDefault()
      this.recordViolation('paste_attempt')
    })
    
    // 标签页切换检测
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        this.recordViolation('tab_switch', { severity: 'high' })
      }
    })
    
    // 窗口失焦检测
    window.addEventListener('blur', () => {
      this.recordViolation('window_blur', { severity: 'medium' })
    })
    
    // 全屏退出检测
    document.addEventListener('fullscreenchange', () => {
      if (!document.fullscreenElement) {
        this.handleFullscreenExit()
      }
    })
    
    // 窗口大小变化检测
    window.addEventListener('resize', () => {
      this.handleWindowResize()
    })
    
    // 键盘事件监控
    document.addEventListener('keydown', (e) => {
      this.handleKeydown(e)
    })
  }
  
  // 禁用快捷键
  private disableShortcuts(): void {
    const blockedCombinations = [
      { ctrl: true, key: 'c' },      // Ctrl+C
      { ctrl: true, key: 'v' },      // Ctrl+V
      { ctrl: true, key: 'a' },      // Ctrl+A
      { ctrl: true, shift: true, key: 'i' }, // Ctrl+Shift+I (DevTools)
      { ctrl: true, shift: true, key: 'j' }, // Ctrl+Shift+J (Console)
      { key: 'F12' },                // F12 (DevTools)
      { ctrl: true, key: 'u' },      // Ctrl+U (View Source)
      { ctrl: true, key: 'p' },      // Ctrl+P (Print)
      { ctrl: true, key: 's' },      // Ctrl+S (Save)
      { alt: true, key: 'Tab' },     // Alt+Tab
      { meta: true, key: 'Tab' },    // Cmd+Tab (Mac)
      { key: 'PrintScreen' }         // PrintScreen
    ]
    
    document.addEventListener('keydown', (e) => {
      for (const combo of blockedCombinations) {
        if (this.matchKeyCombo(e, combo)) {
          e.preventDefault()
          e.stopPropagation()
          this.recordViolation('blocked_shortcut', { 
            key: combo.key,
            severity: 'medium'
          })
          return false
        }
      }
    }, true)
  }
  
  // 环境检测
  private async startEnvironmentDetection(): Promise<void> {
    // 检测虚拟机
    const vmDetected = await this.detectVirtualMachine()
    if (vmDetected) {
      this.recordViolation('virtual_machine', { severity: 'critical' })
    }
    
    // 检测远程桌面
    const rdDetected = await this.detectRemoteDesktop()
    if (rdDetected) {
      this.recordViolation('remote_desktop', { severity: 'critical' })
    }
    
    // 检测多显示器
    const multiDisplay = await this.detectMultipleDisplays()
    if (multiDisplay.count > 1) {
      this.recordViolation('multiple_displays', { 
        severity: 'high',
        count: multiDisplay.count
      })
    }
    
    // 检测截屏软件
    this.startScreenCaptureDetection()
  }
  
  // 虚拟机检测
  private async detectVirtualMachine(): Promise<boolean> {
    const indicators = {
      // WebGL 渲染器检查
      webglVendor: () => {
        const canvas = document.createElement('canvas')
        const gl = canvas.getContext('webgl')
        if (gl) {
          const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
          if (debugInfo) {
            const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)
            const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
            return /virtualbox|vmware|parallels/i.test(vendor + renderer)
          }
        }
        return false
      },
      
      // 屏幕尺寸异常检测
      screenSize: () => {
        const { width, height } = screen
        // 常见虚拟机默认分辨率
        const vmResolutions = [
          [1024, 768],
          [800, 600],
          [1280, 800]
        ]
        return vmResolutions.some(([w, h]) => width === w && height === h)
      },
      
      // 设备内存检测
      deviceMemory: () => {
        // @ts-ignore
        const memory = navigator.deviceMemory
        return memory && memory < 2
      }
    }
    
    let vmScore = 0
    for (const [key, check] of Object.entries(indicators)) {
      if (check()) vmScore++
    }
    
    return vmScore >= 2
  }
  
  // 截屏软件检测
  private startScreenCaptureDetection(): void {
    // 使用 DisplayMediaStreamConstraints 检测
    // 这是一个启发式方法
    setInterval(async () => {
      try {
        // 检查是否有活动的屏幕共享
        const displays = await navigator.mediaDevices.enumerateDevices()
        // 分析设备列表变化
      } catch (e) {
        // 处理错误
      }
    }, 5000)
  }
}

// 前端安全考试组件
const SecureExamComponent = defineComponent({
  name: 'SecureExam',
  setup() {
    const examStore = useExamStore()
    const securityStore = useSecurityStore()
    
    const isLocked = ref(false)
    const violations = ref<Violation[]>([])
    const currentQuestion = ref<Question | null>(null)
    
    // 初始化安全环境
    onMounted(async () => {
      try {
        // 初始化锁定
        const lockdown = new SecureBrowserLockdown()
        await lockdown.initializeLockdown()
        isLocked.value = true
        
        // 监听违规事件
        lockdown.onViolation((violation) => {
          violations.value.push(violation)
          handleViolation(violation)
        })
        
      } catch (error) {
        // 锁定失败,通知监考
        await securityStore.reportSecurityFailure(error)
      }
    })
    
    // 处理违规
    const handleViolation = async (violation: Violation) => {
      // 根据严重程度处理
      switch (violation.severity) {
        case 'critical':
          await examStore.pauseExam('critical_violation')
          break
        case 'high':
          await examStore.issueWarning(violation)
          break
        case 'medium':
        case 'low':
          await examStore.logViolation(violation)
          break
      }
    }
    
    return () => (
      <div class="secure-exam-container">
        {!isLocked.value ? (
          <div class="security-setup">
            <p>正在初始化安全环境...</p>
          </div>
        ) : (
          <div class="exam-content">
            {currentQuestion.value && (
              <QuestionDisplay question={currentQuestion.value} />
            )}
          </div>
        )}
        
        {/* 违规警告弹窗 */}
        <ViolationWarningModal violations={violations.value} />
      </div>
    )
  }
})

2. 摄像头和屏幕录制

// 考试录制服务
class ExamRecordingService {
  private mediaRecorder: MediaRecorder | null = null
  private webcamStream: MediaStream | null = null
  private screenStream: MediaStream | null = null
  private recordedChunks: Blob[] = []
  
  // 初始化录制
  async initializeRecording(examSessionId: string): Promise<void> {
    // 获取摄像头权限
    this.webcamStream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 640 },
        height: { ideal: 480 },
        frameRate: { ideal: 15 }
      },
      audio: true
    })
    
    // 获取屏幕录制权限(如果需要)
    if (this.config.recordScreen) {
      this.screenStream = await navigator.mediaDevices.getDisplayMedia({
        video: true,
        audio: false
      })
    }
    
    // 合并流
    const combinedStream = this.combineStreams(
      this.webcamStream,
      this.screenStream
    )
    
    // 初始化录制器
    this.mediaRecorder = new MediaRecorder(combinedStream, {
      mimeType: 'video/webm;codecs=vp9',
      videoBitsPerSecond: 500000
    })
    
    this.mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        this.recordedChunks.push(event.data)
        // 分段上传
        this.uploadChunk(examSessionId, event.data)
      }
    }
    
    // 开始录制
    this.mediaRecorder.start(30000) // 每30秒一个分片
  }
  
  // 实时视频分析
  async startRealtimeAnalysis(examSessionId: string): Promise<void> {
    const video = document.createElement('video')
    video.srcObject = this.webcamStream
    video.play()
    
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    
    // 定期捕获帧进行分析
    setInterval(async () => {
      canvas.width = video.videoWidth
      canvas.height = video.videoHeight
      ctx?.drawImage(video, 0, 0)
      
      const imageData = canvas.toDataURL('image/jpeg', 0.8)
      
      // 发送到后端分析
      const analysis = await this.analyzeFrame(imageData)
      
      if (analysis.anomalies.length > 0) {
        await this.reportAnomalies(examSessionId, analysis.anomalies)
      }
    }, 2000) // 每2秒分析一帧
  }
  
  // 帧分析
  private async analyzeFrame(imageData: string): Promise<FrameAnalysis> {
    const response = await fetch('/api/proctoring/analyze-frame', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ image: imageData })
    })
    
    return response.json()
  }
  
  // 上传视频分片
  private async uploadChunk(examSessionId: string, chunk: Blob): Promise<void> {
    const formData = new FormData()
    formData.append('video', chunk)
    formData.append('sessionId', examSessionId)
    formData.append('timestamp', Date.now().toString())
    
    await fetch('/api/proctoring/upload-chunk', {
      method: 'POST',
      body: formData
    })
  }
}

AI 监考系统

1. 计算机视觉监控

// AI 监考引擎
class AIProctorEngine {
  private faceDetector: FaceDetector
  private poseEstimator: PoseEstimator
  private objectDetector: ObjectDetector
  private anomalyClassifier: AnomalyClassifier
  
  // 分析视频帧
  async analyzeFrame(frame: ImageData): Promise<FrameAnalysisResult> {
    const results: FrameAnalysisResult = {
      timestamp: Date.now(),
      anomalies: [],
      confidence: 1.0
    }
    
    // 1. 人脸检测
    const faceResult = await this.faceDetector.detect(frame)
    
    if (faceResult.faces.length === 0) {
      results.anomalies.push({
        type: 'no_face_detected',
        severity: 'high',
        confidence: 0.95,
        description: '未检测到人脸'
      })
    } else if (faceResult.faces.length > 1) {
      results.anomalies.push({
        type: 'multiple_faces',
        severity: 'critical',
        confidence: faceResult.confidence,
        description: `检测到${faceResult.faces.length}张人脸`
      })
    }
    
    // 2. 头部姿态估计
    if (faceResult.faces.length === 1) {
      const pose = await this.poseEstimator.estimateHeadPose(
        faceResult.faces[0]
      )
      
      // 检测注视方向异常
      if (this.isGazeAbnormal(pose)) {
        results.anomalies.push({
          type: 'gaze_away',
          severity: 'medium',
          confidence: pose.confidence,
          description: '视线偏离屏幕',
          details: {
            yaw: pose.yaw,
            pitch: pose.pitch
          }
        })
      }
    }
    
    // 3. 物体检测
    const objects = await this.objectDetector.detect(frame)
    
    const suspiciousObjects = objects.filter(obj => 
      this.isSuspiciousObject(obj.label)
    )
    
    for (const obj of suspiciousObjects) {
      results.anomalies.push({
        type: 'suspicious_object',
        severity: 'high',
        confidence: obj.confidence,
        description: `检测到可疑物品:${obj.label}`,
        details: { object: obj }
      })
    }
    
    // 4. 综合异常评分
    results.confidence = this.calculateOverallConfidence(results.anomalies)
    
    return results
  }
  
  // 检测可疑物品
  private isSuspiciousObject(label: string): boolean {
    const suspiciousLabels = [
      'cell phone', 'mobile phone', 'smartphone',
      'book', 'paper', 'note',
      'tablet', 'laptop', 'computer',
      'earphone', 'headphone', 'earbud',
      'smartwatch', 'watch'
    ]
    return suspiciousLabels.includes(label.toLowerCase())
  }
  
  // 视线异常检测
  private isGazeAbnormal(pose: HeadPose): boolean {
    const YAW_THRESHOLD = 30 // 左右转头阈值(度)
    const PITCH_THRESHOLD = 25 // 上下看阈值(度)
    
    return Math.abs(pose.yaw) > YAW_THRESHOLD || 
           Math.abs(pose.pitch) > PITCH_THRESHOLD
  }
  
  // 行为模式分析
  async analyzeBehaviorPattern(
    sessionHistory: SessionHistory
  ): Promise<BehaviorAnalysis> {
    const features = this.extractBehaviorFeatures(sessionHistory)
    
    return {
      // 注意力分析
      attentionScore: this.calculateAttentionScore(features),
      
      // 答题行为分析
      answeringPattern: this.analyzeAnsweringPattern(features),
      
      // 异常行为检测
      anomalies: this.detectBehaviorAnomalies(features),
      
      // 作弊风险评估
      cheatingRiskScore: this.assessCheatingRisk(features)
    }
  }
  
  // 提取行为特征
  private extractBehaviorFeatures(history: SessionHistory): BehaviorFeatures {
    return {
      // 视线偏移频率
      gazeAwayFrequency: this.calculateGazeAwayFrequency(history.gazeEvents),
      
      // 平均答题时间
      avgAnswerTime: this.calculateAvgAnswerTime(history.answers),
      
      // 答题时间方差
      answerTimeVariance: this.calculateAnswerTimeVariance(history.answers),
      
      // 切屏次数
      tabSwitchCount: history.tabSwitches.length,
      
      // 键盘行为模式
      typingPattern: this.analyzeTypingPattern(history.keystrokes),
      
      // 鼠标行为模式
      mousePattern: this.analyzeMousePattern(history.mouseEvents),
      
      // 人脸检测失败次数
      faceDetectionFailures: history.faceEvents.filter(
        e => e.type === 'not_detected'
      ).length
    }
  }
  
  // 作弊风险评估
  private assessCheatingRisk(features: BehaviorFeatures): RiskAssessment {
    const riskFactors: RiskFactor[] = []
    let totalRisk = 0
    
    // 视线异常风险
    if (features.gazeAwayFrequency > 0.3) {
      const risk = Math.min(features.gazeAwayFrequency * 0.8, 0.4)
      riskFactors.push({
        factor: 'gaze_away',
        risk,
        description: '视线频繁偏离屏幕'
      })
      totalRisk += risk
    }
    
    // 切屏风险
    if (features.tabSwitchCount > 3) {
      const risk = Math.min(features.tabSwitchCount * 0.1, 0.4)
      riskFactors.push({
        factor: 'tab_switch',
        risk,
        description: '频繁切换标签页'
      })
      totalRisk += risk
    }
    
    // 答题时间异常风险
    const timeAnomaly = this.detectAnswerTimeAnomaly(features)
    if (timeAnomaly.isAnomalous) {
      riskFactors.push({
        factor: 'answer_time_anomaly',
        risk: timeAnomaly.risk,
        description: timeAnomaly.description
      })
      totalRisk += timeAnomaly.risk
    }
    
    // 人脸检测异常风险
    if (features.faceDetectionFailures > 5) {
      const risk = Math.min(features.faceDetectionFailures * 0.05, 0.3)
      riskFactors.push({
        factor: 'face_detection',
        risk,
        description: '人脸频繁未检测到'
      })
      totalRisk += risk
    }
    
    return {
      totalRisk: Math.min(totalRisk, 1.0),
      riskLevel: this.getRiskLevel(totalRisk),
      factors: riskFactors
    }
  }
  
  // 获取风险等级
  private getRiskLevel(risk: number): 'low' | 'medium' | 'high' | 'critical' {
    if (risk < 0.25) return 'low'
    if (risk < 0.5) return 'medium'
    if (risk < 0.75) return 'high'
    return 'critical'
  }
}

2. 答案相似度检测

// 答案相似度检测服务
class AnswerSimilarityDetector {
  private vectorizer: TextVectorizer
  private plagiarismDetector: PlagiarismDetector
  
  // 检测答案抄袭
  async detectPlagiarism(
    examId: string,
    submissions: ExamSubmission[]
  ): Promise<PlagiarismReport> {
    const results: PlagiarismResult[] = []
    
    // 两两比较所有提交
    for (let i = 0; i < submissions.length; i++) {
      for (let j = i + 1; j < submissions.length; j++) {
        const similarity = await this.compareSubmissions(
          submissions[i],
          submissions[j]
        )
        
        if (similarity.score > 0.7) {
          results.push({
            submission1: submissions[i].userId,
            submission2: submissions[j].userId,
            similarity: similarity.score,
            matchedQuestions: similarity.matchedQuestions,
            evidence: similarity.evidence
          })
        }
      }
    }
    
    return {
      examId,
      totalSubmissions: submissions.length,
      flaggedPairs: results.length,
      results: results.sort((a, b) => b.similarity - a.similarity)
    }
  }
  
  // 比较两份提交
  private async compareSubmissions(
    sub1: ExamSubmission,
    sub2: ExamSubmission
  ): Promise<SimilarityResult> {
    const questionSimilarities: QuestionSimilarity[] = []
    
    for (const questionId of Object.keys(sub1.answers)) {
      const answer1 = sub1.answers[questionId]
      const answer2 = sub2.answers[questionId]
      
      if (!answer1 || !answer2) continue
      
      const similarity = await this.compareAnswers(answer1, answer2)
      
      if (similarity.score > 0.6) {
        questionSimilarities.push({
          questionId,
          similarity: similarity.score,
          type: similarity.type,
          evidence: similarity.evidence
        })
      }
    }
    
    // 计算总体相似度
    const overallScore = questionSimilarities.length > 0
      ? questionSimilarities.reduce((sum, q) => sum + q.similarity, 0) / 
        Object.keys(sub1.answers).length
      : 0
    
    return {
      score: overallScore,
      matchedQuestions: questionSimilarities,
      evidence: this.generateEvidence(questionSimilarities)
    }
  }
  
  // 比较答案
  private async compareAnswers(
    answer1: Answer,
    answer2: Answer
  ): Promise<AnswerComparison> {
    // 对于选择题
    if (answer1.type === 'multiple_choice') {
      return {
        score: answer1.value === answer2.value ? 1 : 0,
        type: 'exact_match',
        evidence: null
      }
    }
    
    // 对于主观题
    if (answer1.type === 'essay' || answer1.type === 'short_answer') {
      // 文本相似度
      const textSimilarity = await this.calculateTextSimilarity(
        answer1.value,
        answer2.value
      )
      
      // 代码相似度(如果是编程题)
      if (answer1.type === 'code') {
        const codeSimilarity = await this.calculateCodeSimilarity(
          answer1.value,
          answer2.value
        )
        
        return {
          score: Math.max(textSimilarity, codeSimilarity),
          type: codeSimilarity > textSimilarity ? 'code_similarity' : 'text_similarity',
          evidence: this.extractSimilarSegments(answer1.value, answer2.value)
        }
      }
      
      return {
        score: textSimilarity,
        type: 'text_similarity',
        evidence: this.extractSimilarSegments(answer1.value, answer2.value)
      }
    }
    
    return { score: 0, type: 'unknown', evidence: null }
  }
  
  // 计算文本相似度
  private async calculateTextSimilarity(
    text1: string,
    text2: string
  ): Promise<number> {
    // 向量化
    const vec1 = await this.vectorizer.vectorize(text1)
    const vec2 = await this.vectorizer.vectorize(text2)
    
    // 余弦相似度
    return this.cosineSimilarity(vec1, vec2)
  }
  
  // 计算代码相似度
  private async calculateCodeSimilarity(
    code1: string,
    code2: string
  ): Promise<number> {
    // 代码标准化
    const normalized1 = this.normalizeCode(code1)
    const normalized2 = this.normalizeCode(code2)
    
    // AST 结构比较
    const astSimilarity = await this.compareAST(normalized1, normalized2)
    
    // token 序列比较
    const tokenSimilarity = await this.compareTokens(normalized1, normalized2)
    
    return Math.max(astSimilarity, tokenSimilarity)
  }
  
  // 代码标准化
  private normalizeCode(code: string): string {
    return code
      // 移除注释
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '')
      // 标准化空白
      .replace(/\s+/g, ' ')
      // 移除变量名差异(简化处理)
      .trim()
  }
}

题库与试卷安全

1. 题库随机化

// 智能题库管理系统
class QuestionBankManager {
  // 生成随机试卷
  async generateRandomizedExam(
    examConfig: ExamConfig
  ): Promise<RandomizedExam> {
    const { 
      questionPool, 
      totalQuestions, 
      distribution,
      constraints 
    } = examConfig
    
    const selectedQuestions: Question[] = []
    
    // 按难度分布选题
    for (const [difficulty, count] of Object.entries(distribution.byDifficulty)) {
      const pool = questionPool.filter(q => q.difficulty === difficulty)
      const selected = this.randomSelect(pool, count)
      selectedQuestions.push(...selected)
    }
    
    // 按知识点分布选题
    if (distribution.byTopic) {
      // 确保各知识点覆盖
      for (const [topic, minCount] of Object.entries(distribution.byTopic)) {
        const topicQuestions = selectedQuestions.filter(
          q => q.topics.includes(topic)
        )
        if (topicQuestions.length < minCount) {
          const additional = this.selectAdditionalByTopic(
            questionPool,
            topic,
            minCount - topicQuestions.length,
            selectedQuestions.map(q => q.id)
          )
          selectedQuestions.push(...additional)
        }
      }
    }
    
    // 打乱题目顺序
    const shuffledQuestions = this.shuffleArray(selectedQuestions)
    
    // 为每道题随机化选项顺序
    const randomizedQuestions = shuffledQuestions.map(q => 
      this.randomizeQuestionOptions(q)
    )
    
    return {
      id: generateId(),
      questions: randomizedQuestions,
      createdAt: new Date(),
      seed: this.currentSeed
    }
  }
  
  // 随机化选项顺序
  private randomizeQuestionOptions(question: Question): Question {
    if (question.type !== 'multiple_choice' && question.type !== 'multiple_select') {
      return question
    }
    
    const options = [...question.options]
    const correctAnswers = question.correctAnswers
    
    // 打乱选项
    const shuffled = this.shuffleArray(options)
    
    // 更新正确答案的位置
    const newCorrectAnswers = correctAnswers.map(ans => {
      const originalIndex = options.findIndex(o => o.id === ans)
      return shuffled.findIndex(o => o.id === options[originalIndex].id)
    })
    
    return {
      ...question,
      options: shuffled,
      correctAnswers: newCorrectAnswers,
      optionMapping: options.map((o, i) => ({
        original: i,
        shuffled: shuffled.findIndex(so => so.id === o.id)
      }))
    }
  }
  
  // 生成参数化题目
  generateParameterizedQuestion(template: QuestionTemplate): Question {
    const parameters = {}
    
    // 生成随机参数
    for (const [param, config] of Object.entries(template.parameters)) {
      if (config.type === 'number') {
        parameters[param] = this.randomNumber(config.min, config.max, config.step)
      } else if (config.type === 'choice') {
        parameters[param] = this.randomChoice(config.options)
      }
    }
    
    // 应用参数到题目
    const questionText = this.applyParameters(template.text, parameters)
    const correctAnswer = this.calculateAnswer(template.answerFormula, parameters)
    
    // 生成干扰项
    const distractors = this.generateDistractors(
      correctAnswer,
      template.distractorRules,
      parameters
    )
    
    return {
      id: generateId(),
      type: template.type,
      text: questionText,
      options: this.shuffleArray([
        { id: 'correct', text: correctAnswer.toString(), isCorrect: true },
        ...distractors.map((d, i) => ({ 
          id: `distractor_${i}`, 
          text: d.toString(), 
          isCorrect: false 
        }))
      ]),
      parameters,
      templateId: template.id
    }
  }
  
  // 生成干扰项
  private generateDistractors(
    correctAnswer: number,
    rules: DistractorRules,
    parameters: Record<string, any>
  ): number[] {
    const distractors: number[] = []
    
    // 常见错误干扰项
    if (rules.commonMistakes) {
      for (const mistake of rules.commonMistakes) {
        const distractor = this.applyMistakeRule(correctAnswer, mistake, parameters)
        if (distractor !== correctAnswer && !distractors.includes(distractor)) {
          distractors.push(distractor)
        }
      }
    }
    
    // 数值接近干扰项
    if (rules.nearbyValues) {
      const nearby = [
        correctAnswer + rules.nearbyValues.offset,
        correctAnswer - rules.nearbyValues.offset,
        correctAnswer * 1.1,
        correctAnswer * 0.9
      ]
      for (const n of nearby) {
        if (n !== correctAnswer && !distractors.includes(Math.round(n))) {
          distractors.push(Math.round(n))
        }
      }
    }
    
    // 确保有足够的干扰项
    while (distractors.length < 3) {
      const random = correctAnswer + this.randomNumber(-100, 100)
      if (random !== correctAnswer && !distractors.includes(random)) {
        distractors.push(random)
      }
    }
    
    return distractors.slice(0, 3)
  }
}

2. 答案防泄露

// 答案安全服务
class AnswerSecurityService {
  private encryptionKey: CryptoKey
  
  // 加密存储答案
  async encryptAnswers(
    examId: string,
    answers: Record<string, any>
  ): Promise<EncryptedAnswers> {
    // 生成随机 IV
    const iv = crypto.getRandomValues(new Uint8Array(12))
    
    // 序列化答案
    const plaintext = JSON.stringify(answers)
    const encoded = new TextEncoder().encode(plaintext)
    
    // AES-GCM 加密
    const ciphertext = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.encryptionKey,
      encoded
    )
    
    return {
      examId,
      ciphertext: Buffer.from(ciphertext).toString('base64'),
      iv: Buffer.from(iv).toString('base64'),
      algorithm: 'AES-GCM-256',
      encryptedAt: new Date()
    }
  }
  
  // 延迟加载答案(只在需要时解密)
  async getAnswerForGrading(
    examId: string,
    questionId: string,
    graderId: string
  ): Promise<Answer> {
    // 验证评分者权限
    await this.verifyGraderPermission(graderId, examId)
    
    // 记录访问日志
    await this.logAnswerAccess(examId, questionId, graderId)
    
    // 获取加密的答案
    const encrypted = await this.getEncryptedAnswers(examId)
    
    // 解密
    const answers = await this.decryptAnswers(encrypted)
    
    return answers[questionId]
  }
  
  // 答案访问审计
  async logAnswerAccess(
    examId: string,
    questionId: string,
    accessorId: string
  ): Promise<void> {
    await this.db.answerAccessLogs.create({
      data: {
        examId,
        questionId,
        accessorId,
        accessedAt: new Date(),
        ipAddress: this.getCurrentIP(),
        userAgent: this.getCurrentUserAgent()
      }
    })
  }
}

// 试卷时间控制
class ExamTimeController {
  // 开始考试
  async startExam(userId: string, examId: string): Promise<ExamSession> {
    const exam = await this.getExam(examId)
    
    // 创建考试会话
    const session: ExamSession = {
      id: generateId(),
      userId,
      examId,
      startedAt: new Date(),
      endsAt: new Date(Date.now() + exam.duration * 60 * 1000),
      status: 'in_progress',
      remainingTime: exam.duration * 60
    }
    
    await this.db.examSessions.create({ data: session })
    
    // 启动服务端计时器
    this.startServerTimer(session.id)
    
    return session
  }
  
  // 服务端计时器
  private async startServerTimer(sessionId: string): Promise<void> {
    const checkInterval = 60 * 1000 // 每分钟检查一次
    
    const timer = setInterval(async () => {
      const session = await this.getSession(sessionId)
      
      if (!session || session.status !== 'in_progress') {
        clearInterval(timer)
        return
      }
      
      // 检查是否超时
      if (new Date() >= new Date(session.endsAt)) {
        await this.forceSubmit(sessionId)
        clearInterval(timer)
      }
    }, checkInterval)
  }
  
  // 强制提交
  async forceSubmit(sessionId: string): Promise<void> {
    const session = await this.getSession(sessionId)
    
    if (session.status !== 'in_progress') {
      return
    }
    
    // 收集当前答案
    const currentAnswers = await this.getCurrentAnswers(sessionId)
    
    // 提交试卷
    await this.submitExam(sessionId, currentAnswers, 'timeout')
    
    // 更新会话状态
    await this.db.examSessions.update({
      where: { id: sessionId },
      data: {
        status: 'completed',
        completedAt: new Date(),
        completionReason: 'timeout'
      }
    })
  }
  
  // 防止时间篡改
  async validateClientTime(
    sessionId: string,
    clientTime: number
  ): Promise<TimeValidation> {
    const serverTime = Date.now()
    const drift = Math.abs(serverTime - clientTime)
    
    // 允许最大 5 秒的时间差
    const MAX_DRIFT = 5000
    
    if (drift > MAX_DRIFT) {
      await this.recordAnomaly(sessionId, {
        type: 'time_drift',
        severity: 'high',
        details: { serverTime, clientTime, drift }
      })
      
      return {
        valid: false,
        serverTime,
        message: '客户端时间与服务器不同步'
      }
    }
    
    return { valid: true, serverTime }
  }
}

监考员后台系统

实时监控仪表盘

<!-- 监考员仪表盘 -->
<template>
  <div class="proctor-dashboard">
    <!-- 顶部统计 -->
    <div class="stats-bar">
      <div class="stat-card">
        <span class="stat-value">{{ activeExaminees }}</span>
        <span class="stat-label">正在考试</span>
      </div>
      <div class="stat-card warning">
        <span class="stat-value">{{ warningCount }}</span>
        <span class="stat-label">警告中</span>
      </div>
      <div class="stat-card danger">
        <span class="stat-value">{{ criticalCount }}</span>
        <span class="stat-label">需要关注</span>
      </div>
      <div class="stat-card">
        <span class="stat-value">{{ completedCount }}</span>
        <span class="stat-label">已完成</span>
      </div>
    </div>
    
    <!-- 考生网格 -->
    <div class="examinees-grid">
      <div 
        v-for="examinee in examinees"
        :key="examinee.id"
        class="examinee-card"
        :class="getRiskClass(examinee.riskLevel)"
        @click="selectExaminee(examinee)"
      >
        <!-- 视频预览 -->
        <div class="video-preview">
          <video 
            :src="examinee.videoStream"
            autoplay
            muted
          />
          <div class="risk-badge" :class="examinee.riskLevel">
            {{ getRiskLabel(examinee.riskLevel) }}
          </div>
        </div>
        
        <!-- 考生信息 -->
        <div class="examinee-info">
          <span class="name">{{ examinee.name }}</span>
          <span class="progress">进度: {{ examinee.progress }}%</span>
        </div>
        
        <!-- 异常指示器 -->
        <div class="anomaly-indicators">
          <span 
            v-for="anomaly in examinee.recentAnomalies.slice(0, 3)"
            :key="anomaly.id"
            class="anomaly-dot"
            :class="anomaly.severity"
            :title="anomaly.description"
          />
        </div>
      </div>
    </div>
    
    <!-- 详情面板 -->
    <div v-if="selectedExaminee" class="detail-panel">
      <div class="panel-header">
        <h3>{{ selectedExaminee.name }}</h3>
        <button @click="selectedExaminee = null">关闭</button>
      </div>
      
      <!-- 大视频 -->
      <div class="main-video">
        <video 
          :src="selectedExaminee.videoStream"
          autoplay
          controls
        />
      </div>
      
      <!-- 异常时间线 -->
      <div class="anomaly-timeline">
        <h4>异常事件</h4>
        <div 
          v-for="event in selectedExaminee.anomalyHistory"
          :key="event.id"
          class="timeline-event"
          :class="event.severity"
        >
          <span class="time">{{ formatTime(event.timestamp) }}</span>
          <span class="description">{{ event.description }}</span>
          <button 
            v-if="event.hasSnapshot"
            @click="viewSnapshot(event)"
          >
            查看截图
          </button>
        </div>
      </div>
      
      <!-- 操作按钮 -->
      <div class="action-buttons">
        <button @click="sendWarning(selectedExaminee.id)">
          发送警告
        </button>
        <button @click="pauseExam(selectedExaminee.id)">
          暂停考试
        </button>
        <button 
          class="danger"
          @click="terminateExam(selectedExaminee.id)"
        >
          终止考试
        </button>
        <button @click="addNote(selectedExaminee.id)">
          添加备注
        </button>
      </div>
    </div>
    
    <!-- 实时警报 -->
    <div class="alert-panel">
      <h4>实时警报</h4>
      <TransitionGroup name="alert">
        <div 
          v-for="alert in recentAlerts"
          :key="alert.id"
          class="alert-item"
          :class="alert.severity"
        >
          <span class="alert-time">{{ formatTime(alert.timestamp) }}</span>
          <span class="alert-examinee">{{ alert.examineeName }}</span>
          <span class="alert-message">{{ alert.message }}</span>
          <button @click="jumpToExaminee(alert.examineeId)">查看</button>
        </div>
      </TransitionGroup>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useProctorStore } from '~/stores/proctor'

const proctorStore = useProctorStore()

const examinees = computed(() => proctorStore.examinees)
const selectedExaminee = ref(null)
const recentAlerts = ref([])

// 统计数据
const activeExaminees = computed(() => 
  examinees.value.filter(e => e.status === 'in_progress').length
)
const warningCount = computed(() =>
  examinees.value.filter(e => e.riskLevel === 'medium' || e.riskLevel === 'high').length
)
const criticalCount = computed(() =>
  examinees.value.filter(e => e.riskLevel === 'critical').length
)
const completedCount = computed(() =>
  examinees.value.filter(e => e.status === 'completed').length
)

// WebSocket 连接
let ws: WebSocket

onMounted(() => {
  // 连接实时监控服务
  ws = new WebSocket(`wss://api.example.com/proctor/realtime`)
  
  ws.onmessage = (event) => {
    const data = JSON.parse(event.data)
    handleRealtimeUpdate(data)
  }
})

onUnmounted(() => {
  ws?.close()
})

const handleRealtimeUpdate = (data: any) => {
  switch (data.type) {
    case 'anomaly':
      // 添加到警报列表
      recentAlerts.value.unshift({
        id: data.id,
        examineeId: data.examineeId,
        examineeName: data.examineeName,
        message: data.message,
        severity: data.severity,
        timestamp: new Date(data.timestamp)
      })
      // 只保留最近 20 条
      recentAlerts.value = recentAlerts.value.slice(0, 20)
      break
      
    case 'status_update':
      proctorStore.updateExamineeStatus(data.examineeId, data.status)
      break
      
    case 'risk_update':
      proctorStore.updateExamineeRisk(data.examineeId, data.riskLevel)
      break
  }
}

const getRiskClass = (level: string) => ({
  'risk-low': level === 'low',
  'risk-medium': level === 'medium',
  'risk-high': level === 'high',
  'risk-critical': level === 'critical'
})

const getRiskLabel = (level: string) => ({
  low: '正常',
  medium: '注意',
  high: '警告',
  critical: '严重'
}[level])

const formatTime = (date: Date) => {
  return new Intl.DateTimeFormat('zh-CN', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  }).format(date)
}

const sendWarning = async (examineeId: string) => {
  await proctorStore.sendWarning(examineeId, '请保持视线在屏幕上')
}

const pauseExam = async (examineeId: string) => {
  await proctorStore.pauseExam(examineeId)
}

const terminateExam = async (examineeId: string) => {
  if (confirm('确定要终止该考生的考试吗?')) {
    await proctorStore.terminateExam(examineeId, '监考员终止')
  }
}

const selectExaminee = (examinee: any) => {
  selectedExaminee.value = examinee
}

const jumpToExaminee = (examineeId: string) => {
  const examinee = examinees.value.find(e => e.id === examineeId)
  if (examinee) {
    selectedExaminee.value = examinee
  }
}
</script>

<style scoped>
.proctor-dashboard {
  display: grid;
  grid-template-columns: 1fr 400px;
  grid-template-rows: auto 1fr;
  gap: 1rem;
  height: 100vh;
  padding: 1rem;
  background: #1a1a2e;
  color: white;
}

.stats-bar {
  grid-column: span 2;
  display: flex;
  gap: 1rem;
}

.stat-card {
  background: #16213e;
  padding: 1rem 1.5rem;
  border-radius: 0.5rem;
  text-align: center;
}

.stat-value {
  font-size: 2rem;
  font-weight: bold;
  display: block;
}

.stat-card.warning .stat-value { color: #f59e0b; }
.stat-card.danger .stat-value { color: #ef4444; }

.examinees-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
  overflow-y: auto;
}

.examinee-card {
  background: #16213e;
  border-radius: 0.5rem;
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.2s;
  border: 2px solid transparent;
}

.examinee-card:hover {
  transform: scale(1.02);
}

.examinee-card.risk-medium { border-color: #f59e0b; }
.examinee-card.risk-high { border-color: #f97316; }
.examinee-card.risk-critical { border-color: #ef4444; }

.video-preview {
  position: relative;
  aspect-ratio: 4/3;
}

.video-preview video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.risk-badge {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  padding: 0.25rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.75rem;
}

.risk-badge.low { background: #10b981; }
.risk-badge.medium { background: #f59e0b; }
.risk-badge.high { background: #f97316; }
.risk-badge.critical { background: #ef4444; }

.alert-panel {
  background: #16213e;
  border-radius: 0.5rem;
  padding: 1rem;
  overflow-y: auto;
}

.alert-item {
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem;
  border-radius: 0.25rem;
  margin-bottom: 0.5rem;
  font-size: 0.875rem;
}

.alert-item.high { background: rgba(249, 115, 22, 0.2); }
.alert-item.critical { background: rgba(239, 68, 68, 0.2); }
</style>

最佳实践与建议

防作弊策略分级

// 不同级别的防作弊策略
const securityLevels = {
  // 低风险考试:练习题、自测
  low: {
    name: '基础防护',
    measures: [
      '基础身份验证(登录密码)',
      '题目随机化',
      '基础时间限制'
    ],
    optional: [
      '简单的切屏检测'
    ]
  },
  
  // 中等风险考试:单元测试、小测验
  medium: {
    name: '标准防护',
    measures: [
      '中等身份验证(密码 + 人脸)',
      '浏览器基础锁定',
      '题目和选项随机化',
      '切屏检测和警告',
      '答案加密存储'
    ],
    optional: [
      '摄像头监控',
      '简单行为分析'
    ]
  },
  
  // 高风险考试:期末考试、重要测评
  high: {
    name: '严格防护',
    measures: [
      '强身份验证(密码 + 人脸 + 身份证)',
      '浏览器完整锁定',
      '持续人脸验证',
      '视频录制',
      'AI 行为分析',
      '环境检测(多显示器、虚拟机)',
      '答案相似度检测'
    ],
    optional: [
      '屏幕录制',
      '实时监考'
    ]
  },
  
  // 极高风险考试:认证考试、职业资格
  critical: {
    name: '最高级防护',
    measures: [
      '全面身份验证(多因素 + 活体检测 + 身份证OCR)',
      '安全浏览器强制使用',
      '全程视频和屏幕录制',
      '实时 AI 监考',
      '人工监考员实时监控',
      '随机身份挑战',
      '全面答案审计',
      '事后人工复核'
    ]
  }
}

隐私与合规

// 隐私保护配置
const privacyConfig = {
  // 数据收集透明度
  dataCollection: {
    // 明确告知收集的数据类型
    disclosedTypes: [
      '摄像头视频(仅考试期间)',
      '屏幕录制(仅考试期间)',
      '键盘和鼠标操作',
      '浏览器和系统信息',
      '网络连接信息'
    ],
    // 使用目的
    purposes: [
      '考试身份验证',
      '作弊行为检测',
      '考试完整性保证'
    ]
  },
  
  // 数据保留政策
  dataRetention: {
    videoRecordings: {
      normalExams: '30天',
      flaggedExams: '1年',
      appealCases: '直到申诉结束后30天'
    },
    behaviorLogs: {
      normalExams: '90天',
      flaggedExams: '1年'
    },
    biometricData: {
      faceEmbeddings: '考试结束后立即删除',
      retainedForVerification: '仅在申诉期间保留'
    }
  },
  
  // 用户权利
  userRights: {
    accessRight: '查看自己的考试记录',
    explanationRight: '获取作弊检测结果的解释',
    appealRight: '对作弊判定提出申诉',
    deletionRight: '请求删除个人数据(在保留期后)'
  }
}

// 考前同意书
 const examConsent = {
  required: true,
  content: `
    在开始考试前,请阅读并同意以下条款:
    
    1. 我同意在考试期间开启摄像头进行身份验证和监控
    2. 我理解我的屏幕可能会被录制用于作弊检测
    3. 我知晓我的操作行为将被记录和分析
    4. 我同意遵守考试规则,不得使用任何作弊手段
    5. 我理解违规行为可能导致考试成绩无效
    
    隐私保护声明:
    - 您的数据将仅用于考试监督目的
    - 数据将根据保留政策安全存储后删除
    - 您有权查看和申诉任何作弊检测结果
  `
}

应急处理机制

// 应急处理流程
class ExamEmergencyHandler {
  // 处理技术故障
  async handleTechnicalIssue(
    sessionId: string,
    issue: TechnicalIssue
  ): Promise<IssueResolution> {
    switch (issue.type) {
      case 'camera_failure':
        // 摄像头故障
        return {
          action: 'pause_and_notify',
          message: '摄像头连接中断,请检查设备',
          graceTime: 300, // 5分钟宽限期
          requiresProctorApproval: true
        }
        
      case 'network_disconnect':
        // 网络断开
        return {
          action: 'auto_save_and_wait',
          message: '网络连接中断,答案已自动保存',
          graceTime: 600, // 10分钟重连时间
          preserveTime: true // 不扣除断网时间
        }
        
      case 'browser_crash':
        // 浏览器崩溃
        return {
          action: 'allow_rejoin',
          message: '可以重新进入考试',
          graceTime: 300,
          requiresIdentityVerification: true
        }
        
      default:
        return {
          action: 'contact_support',
          message: '请联系技术支持'
        }
    }
  }
  
  // 处理作弊警报
  async handleCheatingAlert(
    sessionId: string,
    alert: CheatingAlert
  ): Promise<AlertResponse> {
    // 记录警报
    await this.logAlert(sessionId, alert)
    
    switch (alert.severity) {
      case 'low':
        // 低风险:记录但不干预
        return { action: 'log_only' }
        
      case 'medium':
        // 中等风险:发送警告
        return {
          action: 'warn_user',
          message: '检测到异常行为,请保持视线在屏幕上',
          warningCount: await this.getWarningCount(sessionId)
        }
        
      case 'high':
        // 高风险:严重警告并通知监考
        await this.notifyProctor(sessionId, alert)
        return {
          action: 'severe_warning',
          message: '检测到严重违规行为,监考员已收到通知',
          requiresAcknowledgment: true
        }
        
      case 'critical':
        // 严重风险:暂停考试
        return {
          action: 'pause_exam',
          message: '考试已暂停,请等待监考员处理',
          requiresProctorIntervention: true
        }
    }
  }
}

总结

在线考试防作弊是一个多层次、多维度的系统工程。有效的防作弊方案需要:

  1. 分层防护:根据考试重要性选择合适的防护等级
  2. 多维度检测:结合身份验证、环境监控、行为分析、AI 监考
  3. 平衡体验:在安全性和用户体验之间找到平衡点
  4. 隐私合规:透明的数据收集,合理的数据保留
  5. 应急机制:完善的技术故障和作弊警报处理流程
  6. 持续优化:基于数据分析不断改进检测算法

记住,防作弊的核心目标是保证考试公平,而不是制造不信任的氛围。技术手段应该服务于教育目标,帮助学生展示真实的学习成果。