software copyright
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 自然写互动课堂智慧黑板端应用软件 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<String> = 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<String, Int>, /* 各选项分布 */
|
||||
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<String, StudentAnswer>()
|
||||
|
||||
/** 事件监听器 */
|
||||
private val listeners = CopyOnWriteArrayList<InteractiveListener>()
|
||||
|
||||
/** 倒计时器 */
|
||||
private var countdownTimer: CountDownTimer? = null
|
||||
|
||||
/** 发题时间戳(用于计算学生耗时) */
|
||||
private var publishTimestamp: Long = 0
|
||||
|
||||
/** 历史题目记录 */
|
||||
private val questionHistory = mutableListOf<InteractiveQuestion>()
|
||||
|
||||
/** 历史统计记录 */
|
||||
private val statisticsHistory = mutableListOf<AnswerStatistics>()
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
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<String, Int>()
|
||||
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<String> {
|
||||
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<List<StudentAnswer>> {
|
||||
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<Int, Int> = Pair(answersMap.size, totalStudents)
|
||||
|
||||
/**
|
||||
* 获取历史统计记录
|
||||
*/
|
||||
fun getHistoryStatistics(): List<AnswerStatistics> = statisticsHistory.toList()
|
||||
}
|
||||
Reference in New Issue
Block a user