/** * 自然写互动课堂智慧黑板端应用软件 V1.0 * * InteractiveActivity.kt - 课堂互动答题系统 * * 功能说明: * - 发布互动题目(选择/填空/简答/判断) * - 实时收集学生答案 * - 答题统计与结果展示 * - 随机抽取与分组展示 * - 倒计时控制 * - 答题数据持久化 */ package com.writech.board.ui import android.content.Context import android.os.Bundle import android.os.CountDownTimer import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import kotlin.random.Random /** * 题目类型枚举 */ enum class QuestionType(val code: Int, val label: String) { SINGLE_CHOICE(1, "单选"), MULTIPLE_CHOICE(2, "多选"), TRUE_FALSE(3, "判断"), FILL_BLANK(4, "填空"), SHORT_ANSWER(5, "简答") } /** * 互动题目数据 */ data class InteractiveQuestion( val questionId: String, val type: QuestionType, val title: String, val options: List = emptyList(), /* 选择题选项 */ val correctAnswer: String = "", /* 正确答案 */ val timeLimit: Int = 60, /* 答题时限(秒) */ val score: Int = 10 /* 题目分值 */ ) /** * 学生答案数据 */ data class StudentAnswer( val studentId: String, val studentName: String, val questionId: String, val answer: String, val isCorrect: Boolean = false, val submitTime: Long = System.currentTimeMillis(), val costSeconds: Int = 0 /* 答题耗时(秒) */ ) /** * 答题统计结果 */ data class AnswerStatistics( val questionId: String, val totalStudents: Int, /* 班级总人数 */ val submittedCount: Int, /* 已提交人数 */ val correctCount: Int, /* 正确人数 */ val correctRate: Float, /* 正确率 */ val optionDistribution: Map, /* 各选项分布 */ val avgCostSeconds: Float /* 平均耗时 */ ) /** * 互动答题会话状态 */ enum class SessionState { IDLE, /* 空闲 */ PUBLISHING, /* 发题中 */ ANSWERING, /* 答题中 */ COLLECTING, /* 收卷中 */ REVIEWING /* 查看结果 */ } /** * 互动答题系统事件监听 */ interface InteractiveListener { fun onSessionStateChanged(state: SessionState) fun onAnswerReceived(answer: StudentAnswer) fun onCountdownTick(remainSeconds: Int) fun onCountdownFinished() fun onStatisticsReady(stats: AnswerStatistics) } /** * 课堂互动答题系统 * * 管理整个互动答题流程: * 教师出题 → 发布题目 → 学生作答 → 收卷 → 统计展示 */ class InteractiveManager( private val classroomId: String, private val totalStudents: Int ) { companion object { private const val TAG = "Interactive" } /* ==================== 状态管理 ==================== */ /** 当前会话状态 */ var state: SessionState = SessionState.IDLE private set /** 当前题目 */ private var currentQuestion: InteractiveQuestion? = null /** 学生答案收集: studentId → StudentAnswer */ private val answersMap = ConcurrentHashMap() /** 事件监听器 */ private val listeners = CopyOnWriteArrayList() /** 倒计时器 */ private var countdownTimer: CountDownTimer? = null /** 发题时间戳(用于计算学生耗时) */ private var publishTimestamp: Long = 0 /** 历史题目记录 */ private val questionHistory = mutableListOf() /** 历史统计记录 */ private val statisticsHistory = mutableListOf() /** * 添加事件监听器 */ fun addListener(listener: InteractiveListener) { listeners.add(listener) } /* ==================== 发题流程 ==================== */ /** * 发布互动题目 * 将题目推送给全班学生 * * @param question 题目数据 * @return true=发布成功 */ fun publishQuestion(question: InteractiveQuestion): Boolean { if (state != SessionState.IDLE && state != SessionState.REVIEWING) { Log.w(TAG, "当前状态不允许发题: $state") return false } currentQuestion = question answersMap.clear() publishTimestamp = System.currentTimeMillis() /* 切换状态为发题中 */ changeState(SessionState.PUBLISHING) /* 构建发题消息通过WebSocket推送给学生 */ val msg = buildQuestionMessage(question) Log.i(TAG, "发布题目: ${question.type.label} - ${question.title}") Log.d(TAG, "推送消息: $msg") /* ws.send(msg) - 通过WebSocket推送给网关 */ /* 切换到答题中状态 */ changeState(SessionState.ANSWERING) /* 启动倒计时 */ startCountdown(question.timeLimit) questionHistory.add(question) return true } /** * 构建题目消息JSON */ private fun buildQuestionMessage(question: InteractiveQuestion): String { val sb = StringBuilder() sb.append("{") sb.append("\"type\":\"question\",") sb.append("\"classroom_id\":\"$classroomId\",") sb.append("\"question_id\":\"${question.questionId}\",") sb.append("\"question_type\":${question.type.code},") sb.append("\"title\":\"${question.title}\",") if (question.options.isNotEmpty()) { sb.append("\"options\":[") question.options.forEachIndexed { index, opt -> if (index > 0) sb.append(",") sb.append("\"$opt\"") } sb.append("],") } sb.append("\"time_limit\":${question.timeLimit},") sb.append("\"score\":${question.score},") sb.append("\"timestamp\":${System.currentTimeMillis()}") sb.append("}") return sb.toString() } /* ==================== 答案收集 ==================== */ /** * 接收学生提交的答案 * 通常由WebSocket消息回调触发 */ fun onStudentAnswerReceived(studentId: String, studentName: String, answer: String) { if (state != SessionState.ANSWERING && state != SessionState.COLLECTING) { Log.w(TAG, "非答题状态收到答案, 忽略: student=$studentId") return } val question = currentQuestion ?: return /* 判断答案是否正确 */ val isCorrect = when (question.type) { QuestionType.SINGLE_CHOICE, QuestionType.TRUE_FALSE -> answer.trim().equals(question.correctAnswer.trim(), true) QuestionType.MULTIPLE_CHOICE -> { val submitted = answer.split(",").map { it.trim() }.sorted() val correct = question.correctAnswer.split(",").map { it.trim() }.sorted() submitted == correct } else -> false /* 填空题和简答题需人工批改 */ } /* 计算答题耗时 */ val costSec = ((System.currentTimeMillis() - publishTimestamp) / 1000).toInt() val studentAnswer = StudentAnswer( studentId = studentId, studentName = studentName, questionId = question.questionId, answer = answer, isCorrect = isCorrect, costSeconds = costSec ) answersMap[studentId] = studentAnswer /* 通知监听器 */ listeners.forEach { it.onAnswerReceived(studentAnswer) } Log.d(TAG, "收到答案: $studentName ($studentId) = $answer, " + "正确=$isCorrect, 耗时=${costSec}s, " + "进度=${answersMap.size}/$totalStudents") /* 检查是否全部提交 */ if (answersMap.size >= totalStudents) { Log.i(TAG, "全部学生已提交, 自动收卷") collectAnswers() } } /* ==================== 收卷与统计 ==================== */ /** * 手动收卷(教师点击收卷按钮) */ fun collectAnswers() { if (state != SessionState.ANSWERING) { Log.w(TAG, "非答题状态无法收卷") return } /* 停止倒计时 */ countdownTimer?.cancel() changeState(SessionState.COLLECTING) /* 发送收卷指令给学生端 */ /* ws.send("{\"type\":\"collect\",\"question_id\":\"...\"}") */ Log.i(TAG, "收卷完成: 已提交=${answersMap.size}/$totalStudents") /* 生成统计结果 */ val stats = generateStatistics() statisticsHistory.add(stats) /* 切换到查看结果状态 */ changeState(SessionState.REVIEWING) listeners.forEach { it.onStatisticsReady(stats) } } /** * 生成答题统计结果 */ private fun generateStatistics(): AnswerStatistics { val question = currentQuestion ?: return AnswerStatistics( "", totalStudents, 0, 0, 0f, emptyMap(), 0f ) val answers = answersMap.values.toList() val correctCount = answers.count { it.isCorrect } val correctRate = if (answers.isNotEmpty()) { correctCount.toFloat() / answers.size } else 0f val avgCost = if (answers.isNotEmpty()) { answers.map { it.costSeconds }.average().toFloat() } else 0f /* 统计各选项分布(选择题) */ val distribution = mutableMapOf() if (question.type == QuestionType.SINGLE_CHOICE || question.type == QuestionType.TRUE_FALSE) { answers.forEach { ans -> distribution[ans.answer] = (distribution[ans.answer] ?: 0) + 1 } } val stats = AnswerStatistics( questionId = question.questionId, totalStudents = totalStudents, submittedCount = answers.size, correctCount = correctCount, correctRate = correctRate, optionDistribution = distribution, avgCostSeconds = avgCost ) Log.i(TAG, "统计结果: 提交${answers.size}/${totalStudents}, " + "正确率=${String.format("%.1f", correctRate * 100)}%, " + "平均耗时=${String.format("%.1f", avgCost)}s") return stats } /* ==================== 随机抽取 ==================== */ /** * 随机抽取指定数量的学生 * 用于课堂随机点名展示 */ fun randomPickStudents(count: Int): List { val allStudents = answersMap.keys.toList() if (allStudents.size <= count) return allStudents return allStudents.shuffled(Random(System.currentTimeMillis())).take(count).also { Log.i(TAG, "随机抽取${count}名学生: $it") } } /** * 按分组展示学生答案 * @param groupSize 每组人数 */ fun groupStudents(groupSize: Int): List> { val answers = answersMap.values.toList() return answers.chunked(groupSize).also { Log.i(TAG, "分组展示: ${it.size}组, 每组${groupSize}人") } } /* ==================== 倒计时 ==================== */ /** * 启动答题倒计时 */ private fun startCountdown(seconds: Int) { countdownTimer?.cancel() countdownTimer = object : CountDownTimer(seconds * 1000L, 1000) { override fun onTick(millisUntilFinished: Long) { val remain = (millisUntilFinished / 1000).toInt() listeners.forEach { it.onCountdownTick(remain) } } override fun onFinish() { Log.i(TAG, "答题时间到") listeners.forEach { it.onCountdownFinished() } collectAnswers() } }.start() Log.i(TAG, "倒计时启动: ${seconds}秒") } /* ==================== 状态管理 ==================== */ /** * 变更会话状态 */ private fun changeState(newState: SessionState) { val oldState = state state = newState Log.d(TAG, "状态变更: $oldState → $newState") listeners.forEach { it.onSessionStateChanged(newState) } } /** * 重置为空闲状态 */ fun reset() { countdownTimer?.cancel() answersMap.clear() currentQuestion = null changeState(SessionState.IDLE) Log.i(TAG, "互动系统已重置") } /** * 获取当前提交进度 (已提交/总人数) */ fun getProgress(): Pair = Pair(answersMap.size, totalStudents) /** * 获取历史统计记录 */ fun getHistoryStatistics(): List = statisticsHistory.toList() }