Files
system-design/software-copyright/09-writech-app-board/ui/InteractiveActivity.kt
T
2026-03-22 15:24:40 +08:00

430 lines
13 KiB
Kotlin

/**
* 自然写互动课堂智慧黑板端应用软件 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()
}