修复小bug,剩余语音未测试

This commit is contained in:
xiao12feng8 2025-12-07 01:19:40 +08:00
parent 1a9423951c
commit 1dc2883922
9 changed files with 191 additions and 49 deletions

View File

@ -505,23 +505,25 @@ public class StudyScoreController extends BaseController
return "FALSE"; return "FALSE";
} }
// 修复去除所有空格确保格式一致
String noSpaces = trimmed.replaceAll("\\s+", "");
// 处理选项前缀去除"A. ""B. "等前缀 // 处理选项前缀去除"A. ""B. "等前缀
// 支持格式 // 支持格式
// - "A. 选项内容" -> "A" "选项内容" // - "A.选项内容" -> "A" "选项内容"已去除空格
// - "A.选项内容" -> "A" "选项内容"
// - "A" -> "A" // - "A" -> "A"
String normalized = trimmed; String normalized = noSpaces;
// 如果以"字母. ""字母."开头提取字母部分 // 如果以"字母."开头提取字母部分
if (trimmed.matches("^[A-F]\\.\\s*.*")) if (noSpaces.matches("^[A-F]\\..*"))
{ {
// 提取字母 "A. 选项内容" -> "A" // 提取字母 "A.选项内容" -> "A"
normalized = trimmed.substring(0, 1).toUpperCase(); normalized = noSpaces.substring(0, 1).toUpperCase();
} }
else if (trimmed.length() == 1 && trimmed.matches("[A-Fa-f]")) else if (noSpaces.length() == 1 && noSpaces.matches("[A-Fa-f]"))
{ {
// 单个字母 "A" -> "A" // 单个字母 "A" -> "A"
normalized = trimmed.toUpperCase(); normalized = noSpaces.toUpperCase();
} }
return normalized; return normalized;
@ -540,9 +542,13 @@ public class StudyScoreController extends BaseController
*/ */
private boolean compareMultipleAnswers(String studentAnswer, String correctAnswer, String optionsJson) private boolean compareMultipleAnswers(String studentAnswer, String correctAnswer, String optionsJson)
{ {
// 修复去除所有空格确保格式一致与保存题目时的规范化逻辑一致
String normalizedStudentAnswer = studentAnswer.replaceAll("\\s+", "");
String normalizedCorrectAnswer = correctAnswer.replaceAll("\\s+", "");
// 使用逗号分隔不使用空格分号等避免选项内容中的这些字符导致问题 // 使用逗号分隔不使用空格分号等避免选项内容中的这些字符导致问题
String[] studentParts = studentAnswer.split(","); String[] studentParts = normalizedStudentAnswer.split(",");
String[] correctParts = correctAnswer.split(","); String[] correctParts = normalizedCorrectAnswer.split(",");
java.util.Set<String> studentSet = new java.util.HashSet<>(); java.util.Set<String> studentSet = new java.util.HashSet<>();
java.util.Set<String> correctSet = new java.util.HashSet<>(); java.util.Set<String> correctSet = new java.util.HashSet<>();

View File

@ -33,6 +33,12 @@ public class StudyLearningRecord extends BaseEntity
/** 课程名称(不持久化,仅用于显示) */ /** 课程名称(不持久化,仅用于显示) */
private String courseName; private String courseName;
/** 学生姓名(不持久化,仅用于搜索和显示) */
private String studentName;
/** 学生学号/信息编号(不持久化,仅用于搜索和显示) */
private String studentNo;
/** 学习进度百分比0-100 */ /** 学习进度百分比0-100 */
@Excel(name = "学习进度") @Excel(name = "学习进度")
private BigDecimal progress; private BigDecimal progress;
@ -172,6 +178,26 @@ public class StudyLearningRecord extends BaseEntity
this.courseName = courseName; this.courseName = courseName;
} }
public String getStudentName()
{
return studentName;
}
public void setStudentName(String studentName)
{
this.studentName = studentName;
}
public String getStudentNo()
{
return studentNo;
}
public void setStudentNo(String studentNo)
{
this.studentNo = studentNo;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)

View File

@ -51,6 +51,7 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
@Override @Override
public StudyLearningRecord selectLearningRecordById(Long id) public StudyLearningRecord selectLearningRecordById(Long id)
{ {
// 直接返回数据库中的进度值与APP端保持一致
return learningRecordMapper.selectLearningRecordById(id); return learningRecordMapper.selectLearningRecordById(id);
} }
@ -65,15 +66,9 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
{ {
List<StudyLearningRecord> records = learningRecordMapper.selectLearningRecordList(learningRecord); List<StudyLearningRecord> records = learningRecordMapper.selectLearningRecordList(learningRecord);
// 实时计算每条记录的进度与前端保持一致 // 直接返回数据库中的进度值与APP端保持一致
for (StudyLearningRecord record : records) // APP端会在学习时计算进度并上报保存后台直接显示保存的值即可
{ logger.info("【管理端查询】查询到 {} 条学习记录,直接使用数据库中的进度值", records.size());
if (record.getStudentId() != null && record.getCourseId() != null)
{
BigDecimal realTimeProgress = calculateCourseProgressForDisplay(record.getStudentId(), record.getCourseId());
record.setProgress(realTimeProgress);
}
}
return records; return records;
} }
@ -84,6 +79,8 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
*/ */
private BigDecimal calculateCourseProgressForDisplay(Long studentId, Long courseId) private BigDecimal calculateCourseProgressForDisplay(Long studentId, Long courseId)
{ {
logger.info("【进度计算】开始计算 - 学生ID={}, 课程ID={}", studentId, courseId);
// 查询课程包含的所有课件 // 查询课程包含的所有课件
StudyCourseware query = new StudyCourseware(); StudyCourseware query = new StudyCourseware();
query.setCourseId(courseId); query.setCourseId(courseId);
@ -91,10 +88,12 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
if (allCoursewareList == null || allCoursewareList.isEmpty()) if (allCoursewareList == null || allCoursewareList.isEmpty())
{ {
logger.warn("【进度计算】课程{}没有课件", courseId);
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
int totalCoursewareCount = allCoursewareList.size(); int totalCoursewareCount = allCoursewareList.size();
logger.info("【进度计算】课程共有{}个课件", totalCoursewareCount);
// 查询该学员对该课程的所有学习详情记录 // 查询该学员对该课程的所有学习详情记录
StudyLearningDetail detailQuery = new StudyLearningDetail(); StudyLearningDetail detailQuery = new StudyLearningDetail();
@ -136,6 +135,9 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
{ {
StudyLearningDetail detail = coursewareDetailMap.get(cw.getId()); StudyLearningDetail detail = coursewareDetailMap.get(cw.getId());
logger.info("【课件检查】课件ID={}, 类型={}, 标题={}, 配置时长={}秒",
cw.getId(), cw.getType(), cw.getTitle(), cw.getDuration());
if ("video".equals(cw.getType())) if ("video".equals(cw.getType()))
{ {
// 视频判断是否完成 // 视频判断是否完成
@ -145,6 +147,9 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
int maxPosition = maxPositionMap.getOrDefault(cw.getId(), videoPosition); int maxPosition = maxPositionMap.getOrDefault(cw.getId(), videoPosition);
int duration = (cw.getDuration() != null && cw.getDuration() > 0) ? cw.getDuration() : 0; int duration = (cw.getDuration() != null && cw.getDuration() > 0) ? cw.getDuration() : 0;
logger.info("【视频课件】课件ID={}, 播放位置={}秒, 最大位置={}秒, 配置时长={}秒",
cw.getId(), videoPosition, maxPosition, duration);
// 推断真实时长 // 推断真实时长
int realDuration = duration > 0 ? duration : (maxPosition + 3); int realDuration = duration > 0 ? duration : (maxPosition + 3);
@ -155,11 +160,19 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
if (videoPosition == maxPosition && videoPosition >= 3) if (videoPosition == maxPosition && videoPosition >= 3)
{ {
isCompleted = true; isCompleted = true;
logger.info("【完成判断】课件ID={} - 特殊判断通过(播放到最大位置{}秒)", cw.getId(), videoPosition);
} }
// 常规判断观看进度 >= 90% // 常规判断观看进度 >= 90%
else if (realDuration > 0 && videoPosition >= realDuration * 0.9) else if (realDuration > 0 && videoPosition >= realDuration * 0.9)
{ {
isCompleted = true; isCompleted = true;
logger.info("【完成判断】课件ID={} - 常规判断通过({}秒 >= {}秒的90%",
cw.getId(), videoPosition, realDuration);
}
else
{
logger.info("【完成判断】课件ID={} - 未完成({}秒 < {}秒的90%",
cw.getId(), videoPosition, realDuration);
} }
if (isCompleted) if (isCompleted)
@ -167,6 +180,11 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
completedCount++; completedCount++;
} }
} }
else
{
logger.info("【视频课件】课件ID={} - 无学习记录或videoPosition为null, detail={}",
cw.getId(), detail);
}
} }
else else
{ {
@ -174,6 +192,11 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
if (detail != null) if (detail != null)
{ {
completedCount++; completedCount++;
logger.info("【非视频课件】课件ID={} - 已完成", cw.getId());
}
else
{
logger.info("【非视频课件】课件ID={} - 无学习记录", cw.getId());
} }
} }
} }
@ -183,9 +206,15 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
{ {
double completionRate = (double) completedCount / totalCoursewareCount * 100; double completionRate = (double) completedCount / totalCoursewareCount * 100;
BigDecimal progress = new BigDecimal(completionRate).setScale(2, BigDecimal.ROUND_HALF_UP); BigDecimal progress = new BigDecimal(completionRate).setScale(2, BigDecimal.ROUND_HALF_UP);
return progress.compareTo(new BigDecimal(100)) > 0 ? new BigDecimal(100) : progress; BigDecimal finalProgress = progress.compareTo(new BigDecimal(100)) > 0 ? new BigDecimal(100) : progress;
logger.info("【进度计算】完成 - 学生ID={}, 课程ID={}, 已完成{}/总共{} = {}%",
studentId, courseId, completedCount, totalCoursewareCount, finalProgress);
return finalProgress;
} }
logger.warn("【进度计算】总课件数为0 - 学生ID={}, 课程ID={}", studentId, courseId);
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
@ -198,6 +227,7 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
@Override @Override
public List<StudyLearningRecord> selectLearningRecordListByStudentId(Long studentId) public List<StudyLearningRecord> selectLearningRecordListByStudentId(Long studentId)
{ {
// 直接返回数据库中的进度值与APP端保持一致
return learningRecordMapper.selectLearningRecordListByStudentId(studentId); return learningRecordMapper.selectLearningRecordListByStudentId(studentId);
} }
@ -210,6 +240,7 @@ public class StudyLearningRecordServiceImpl implements IStudyLearningRecordServi
@Override @Override
public List<StudyLearningRecord> selectLearningRecordListByCourseId(Long courseId) public List<StudyLearningRecord> selectLearningRecordListByCourseId(Long courseId)
{ {
// 直接返回数据库中的进度值与APP端保持一致
return learningRecordMapper.selectLearningRecordListByCourseId(courseId); return learningRecordMapper.selectLearningRecordListByCourseId(courseId);
} }

View File

@ -9,6 +9,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="studentId" column="student_id" /> <result property="studentId" column="student_id" />
<result property="courseId" column="course_id" /> <result property="courseId" column="course_id" />
<result property="courseName" column="course_name" /> <result property="courseName" column="course_name" />
<result property="studentName" column="student_name" />
<result property="studentNo" column="student_no" />
<result property="progress" column="progress" /> <result property="progress" column="progress" />
<result property="totalDuration" column="total_duration" /> <result property="totalDuration" column="total_duration" />
<result property="learnCount" column="learn_count" /> <result property="learnCount" column="learn_count" />
@ -24,9 +26,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectLearningRecordVo"> <sql id="selectLearningRecordVo">
select lr.id, lr.student_id, lr.course_id, c.course_name, lr.progress, lr.total_duration, lr.learn_count, lr.video_progress, lr.video_total_duration, lr.last_learn_time, lr.last_video_position, lr.create_by, lr.create_time, lr.update_by, lr.update_time, lr.remark select lr.id, lr.student_id, lr.course_id, c.course_name,
u.nick_name as student_name, u.user_id as student_no,
lr.progress, lr.total_duration, lr.learn_count, lr.video_progress,
lr.video_total_duration, lr.last_learn_time, lr.last_video_position,
lr.create_by, lr.create_time, lr.update_by, lr.update_time, lr.remark
from learning_record lr from learning_record lr
left join course c on lr.course_id = c.id left join course c on lr.course_id = c.id
left join sys_user u on lr.student_id = u.user_id
</sql> </sql>
<select id="selectLearningRecordList" parameterType="StudyLearningRecord" resultMap="StudyLearningRecordResult"> <select id="selectLearningRecordList" parameterType="StudyLearningRecord" resultMap="StudyLearningRecordResult">
@ -38,6 +45,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="courseId != null"> <if test="courseId != null">
and lr.course_id = #{courseId} and lr.course_id = #{courseId}
</if> </if>
<if test="studentName != null and studentName != ''">
and u.nick_name like concat('%', #{studentName}, '%')
</if>
<if test="studentNo != null and studentNo != ''">
and u.user_id = #{studentNo}
</if>
<!-- 数据范围过滤 --> <!-- 数据范围过滤 -->
${params.dataScope} ${params.dataScope}
</where> </where>
@ -46,24 +59,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectLearningRecordById" parameterType="Long" resultMap="StudyLearningRecordResult"> <select id="selectLearningRecordById" parameterType="Long" resultMap="StudyLearningRecordResult">
<include refid="selectLearningRecordVo"/> <include refid="selectLearningRecordVo"/>
where id = #{id} where lr.id = #{id}
</select> </select>
<select id="selectLearningRecordByStudentAndCourse" resultMap="StudyLearningRecordResult"> <select id="selectLearningRecordByStudentAndCourse" resultMap="StudyLearningRecordResult">
<include refid="selectLearningRecordVo"/> <include refid="selectLearningRecordVo"/>
where student_id = #{studentId} and course_id = #{courseId} limit 1 where lr.student_id = #{studentId} and lr.course_id = #{courseId} limit 1
</select> </select>
<select id="selectLearningRecordListByStudentId" parameterType="Long" resultMap="StudyLearningRecordResult"> <select id="selectLearningRecordListByStudentId" parameterType="Long" resultMap="StudyLearningRecordResult">
<include refid="selectLearningRecordVo"/> <include refid="selectLearningRecordVo"/>
where student_id = #{studentId} where lr.student_id = #{studentId}
order by last_learn_time desc order by lr.last_learn_time desc
</select> </select>
<select id="selectLearningRecordListByCourseId" parameterType="Long" resultMap="StudyLearningRecordResult"> <select id="selectLearningRecordListByCourseId" parameterType="Long" resultMap="StudyLearningRecordResult">
<include refid="selectLearningRecordVo"/> <include refid="selectLearningRecordVo"/>
where course_id = #{courseId} where lr.course_id = #{courseId}
order by last_learn_time desc order by lr.last_learn_time desc
</select> </select>
<insert id="insertLearningRecord" parameterType="StudyLearningRecord" useGeneratedKeys="true" keyProperty="id"> <insert id="insertLearningRecord" parameterType="StudyLearningRecord" useGeneratedKeys="true" keyProperty="id">

View File

@ -1,12 +1,21 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="课程ID" prop="courseId"> <el-form-item label="罪犯姓名" prop="studentName">
<el-input <el-input
v-model="queryParams.courseId" v-model="queryParams.studentName"
placeholder="请输入课程ID" placeholder="请输入罪犯姓名"
clearable clearable
style="width: 240px" style="width: 200px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="信息编号" prop="studentNo">
<el-input
v-model="queryParams.studentNo"
placeholder="请输入信息编号"
clearable
style="width: 200px"
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
@ -31,10 +40,12 @@
</el-row> </el-row>
<el-table v-loading="loading" :data="recordList"> <el-table v-loading="loading" :data="recordList">
<el-table-column label="记录ID" align="center" prop="id" /> <el-table-column label="记录ID" align="center" prop="id" width="80" />
<el-table-column label="学员ID" align="center" prop="studentId" /> <el-table-column label="学员ID" align="center" prop="studentId" width="100" />
<el-table-column label="罪犯姓名" align="center" prop="studentName" width="120" />
<el-table-column label="信息编号" align="center" prop="studentNo" width="120" />
<el-table-column label="课程名称" align="center" prop="courseName" min-width="150" /> <el-table-column label="课程名称" align="center" prop="courseName" min-width="150" />
<el-table-column label="学习次数" align="center" prop="learnCount" /> <el-table-column label="学习次数" align="center" prop="learnCount" width="100" />
<el-table-column label="累计时长" align="center" prop="totalDuration"> <el-table-column label="累计时长" align="center" prop="totalDuration">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDuration(scope.row.totalDuration) }}</span> <span>{{ formatDuration(scope.row.totalDuration) }}</span>
@ -86,7 +97,8 @@ export default {
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
courseId: null studentName: null,
studentNo: null
}, },
// //
refreshing: false refreshing: false

View File

@ -1,7 +1,7 @@
<script> <script>
import config from '@/utils/config.js' import config from '@/utils/config.js'
const APP_DEV_HOST = '192.168.137.1' const APP_DEV_HOST = '192.168.1.80'
const APP_DEV_PORT = 30091 const APP_DEV_PORT = 30091
export default { export default {
@ -12,8 +12,8 @@
// App使 // App使
// #ifdef APP-PLUS // #ifdef APP-PLUS
// 使 192.168.137.1 // 使 192.168.1.80
const PROD_HOST = '192.168.137.1' const PROD_HOST = '192.168.1.80'
const PROD_PORT = 30091 const PROD_PORT = 30091
// //

View File

@ -392,8 +392,12 @@ export default {
answerArray.push(option) answerArray.push(option)
} }
this.$set(this.answers, questionId, answerArray.join(',')) // 使
const sortedAnswer = answerArray.sort().join(',')
this.$set(this.answers, questionId, sortedAnswer)
this.saveAnswers() this.saveAnswers()
console.log(`多选题答案更新 - 题目${questionId}:`, sortedAnswer)
}, },
isOptionSelected(questionId, option) { isOptionSelected(questionId, option) {
@ -498,9 +502,15 @@ export default {
console.warn('题目数据不完整:', question) console.warn('题目数据不完整:', question)
return null return null
} }
const answer = this.answers[question.id] || ''
console.log(`题目${question.id} [${question.questionType}]:`, {
questionContent: question.questionContent,
userAnswer: answer,
correctAnswer: question.correctAnswer
})
return { return {
questionId: question.id, questionId: question.id,
answer: this.answers[question.id] || '' answer: answer
} }
}).filter(item => item !== null) // }).filter(item => item !== null) //

View File

@ -264,10 +264,7 @@ export default {
} }
// #ifdef APP-PLUS // #ifdef APP-PLUS
// 使UTS // 使UTS
speechRecorder.init() this.initSpeechService()
this.statusText = '准备就绪'
this.isReady = true
console.log('[Speech] 使用服务器端语音识别')
// #endif // #endif
// #ifndef APP-PLUS // #ifndef APP-PLUS
this.statusText = '语音识别仅支持APP端' this.statusText = '语音识别仅支持APP端'

View File

@ -70,14 +70,23 @@ class SpeechRecorder {
stop() { stop() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.isRecording) { if (!this.isRecording) {
reject(new Error('未在录音中')) // 如果已经停止了,但有临时文件,返回该文件
if (this.tempFilePath) {
resolve(this.tempFilePath)
} else {
reject(new Error('未在录音中'))
}
return return
} }
this.recorderManager.onStop((res) => { // 注册一次性监听器
const onStopHandler = (res) => {
this.tempFilePath = res.tempFilePath
this.isRecording = false
resolve(res.tempFilePath) resolve(res.tempFilePath)
}) }
this.recorderManager.onStop(onStopHandler)
this.recorderManager.stop() this.recorderManager.stop()
}) })
} }
@ -92,6 +101,8 @@ class SpeechRecorder {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 获取服务器配置 // 获取服务器配置
const config = require('./config.js').default const config = require('./config.js').default
// 开发环境可以用 localhost:5000 测试
// const serverUrl = 'http://localhost:5000' // Windows本地测试
const serverUrl = config.API_BASE_URL const serverUrl = config.API_BASE_URL
uni.uploadFile({ uni.uploadFile({
@ -157,6 +168,42 @@ class SpeechRecorder {
} }
} }
} }
/**
* 异步评测方法用于已停止录音的场景
* @param {string} referenceText 参考文本
* @param {number} questionId 题目ID可选
* @returns {Promise<Object>} 评测结果
*/
async evaluateAsync(referenceText, questionId = null) {
try {
console.log('[语音评测] 开始异步评测', { referenceText, questionId, tempFilePath: this.tempFilePath })
// 检查是否有录音文件
if (!this.tempFilePath) {
throw new Error('没有可用的录音文件')
}
// 上传并识别
const result = await this.uploadAndRecognize(this.tempFilePath, {
referenceText: referenceText,
questionId: questionId
})
console.log('[语音评测] 评测成功', result)
return {
success: true,
...result
}
} catch (error) {
console.error('[语音评测] 评测失败', error)
return {
success: false,
error: error.message || '评测失败'
}
}
}
} }
// 导出单例 // 导出单例