Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -5,13 +5,15 @@ FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
|
||||
|
||||
LABEL maintainer="shihongwei"
|
||||
|
||||
RUN dnf install -y ffmpeg && dnf clean all
|
||||
|
||||
RUN mkdir -p /hot-platform/server/logs \
|
||||
/hot-platform/server/temp \
|
||||
/hot-platform/skywalking/agent
|
||||
|
||||
WORKDIR /hot-platform/server
|
||||
|
||||
ENV SERVER_PORT=19090 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="" TZ=Asia/Shanghai
|
||||
ENV SERVER_PORT=19090 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="" TZ=Asia/Shanghai VIDEO_UPLOAD_FFMPEG_COMMAND=ffmpeg
|
||||
|
||||
EXPOSE ${SERVER_PORT}
|
||||
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
|
||||
|
||||
@@ -42,7 +42,7 @@ public class TrainingPlanMonthlyStatProvider implements IReportDataProvider {
|
||||
private final IHotTrainingCourseConfigService hotTrainingCourseConfigService;
|
||||
private final IHotCourseResourceService hotCourseResourceService;
|
||||
private final HotMediaResourceMapper hotMediaResourceMapper;
|
||||
|
||||
|
||||
@Override
|
||||
public String getTemplateCode() {
|
||||
return "templates/securityManage/onlineEducationMonthlyStatistics.html.vm";
|
||||
@@ -78,12 +78,12 @@ public class TrainingPlanMonthlyStatProvider implements IReportDataProvider {
|
||||
ctx.put("trainingCourses", buildTrainingCourses(trainingId));
|
||||
ctx.put("trainingContent", buildTrainingContent(trainingId));
|
||||
|
||||
ctx.put("completedCount", training.getCompleteCount() == null ? 0L : training.getCompleteCount());
|
||||
List<OnlineTrainingMonthlyRowVo> voRows = ledgerReportMapper.selectOnlineTrainingMonthlyRows(companyId, trainingId, normalizedUserIds);
|
||||
if (voRows == null || voRows.isEmpty()) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
ctx.put("completedCount", countCompleted(voRows));
|
||||
ctx.put("rows", buildRows(voRows, progressMap));
|
||||
return ctx;
|
||||
}
|
||||
|
||||
@@ -293,6 +293,9 @@ public class HotTrainingAnswerRecordServiceImpl implements IHotTrainingAnswerRec
|
||||
// 更新
|
||||
HotTrainingAnswerRecord existing = existingMap.get(entity.getQuestionSource());
|
||||
entity.setId(existing.getId());
|
||||
|
||||
mergeErrorData(entity, existing);
|
||||
|
||||
toUpdate.add(entity);
|
||||
} else {
|
||||
// 插入
|
||||
@@ -506,4 +509,18 @@ public class HotTrainingAnswerRecordServiceImpl implements IHotTrainingAnswerRec
|
||||
}
|
||||
throw new ServiceException("无法获取提交人姓名,签名校验失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 累加错误次数并拼接最新的错误答案
|
||||
* 拼接后的总长度控制在200字符内,保留最新产生的数据(先不要)
|
||||
*
|
||||
* @param entity 当前传入的答题实体
|
||||
* @param existing 数据库中已存在的答题实体
|
||||
*/
|
||||
private void mergeErrorData(HotTrainingAnswerRecord entity, HotTrainingAnswerRecord existing) {
|
||||
// 1. 累加错误次数,前端提交一次答题,只会加一次
|
||||
int currentErrorCount = entity.getErrorCount() != null ? 1 : 0;
|
||||
int existingErrorCount = existing.getErrorCount() != null ? existing.getErrorCount() : 0;
|
||||
entity.setErrorCount(existingErrorCount + currentErrorCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ public class HotTrainingCourseConfigController extends BaseController {
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "分页查询企业课程配置列表")
|
||||
public TableDataInfo<HotTrainingCourseConfigVo> list(HotTrainingCourseConfigBo bo, PageQuery pageQuery) {
|
||||
if (bo.getTrainingId() != null && StringUtils.isNotBlank(bo.getUserId())
|
||||
// TODO
|
||||
if (!Boolean.TRUE.equals(bo.getSkipIntercept()) && bo.getTrainingId() != null && StringUtils.isNotBlank(bo.getUserId())
|
||||
&& !isDriverTrainingAccessible(bo.getCompanyId(), bo.getTrainingId(), bo.getUserId())) {
|
||||
TableDataInfo<HotTrainingCourseConfigVo> result = TableDataInfo.build(Collections.emptyList());
|
||||
result.setMsg("当前学习计划已关闭或已过期");
|
||||
|
||||
@@ -82,4 +82,9 @@ public class HotTrainingCourseConfigBo extends BaseEntity {
|
||||
private String updateByName;
|
||||
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 是否跳过过期/关闭拦截(用于后台详情展示等场景)
|
||||
*/
|
||||
private Boolean skipIntercept;
|
||||
}
|
||||
|
||||
@@ -186,6 +186,12 @@ public class HotTrainingCourseRecordServiceImpl implements IHotTrainingCourseRec
|
||||
update.setProgressRate(before.getProgressRate());
|
||||
}
|
||||
}
|
||||
if (before != null && before.getLearnDurationMin() != null && update.getLearnDurationMin() != null) {
|
||||
// 确保学习时长(秒)单调不下降,避免前端误传0导致统计为0
|
||||
if (update.getLearnDurationMin() < before.getLearnDurationMin()) {
|
||||
update.setLearnDurationMin(before.getLearnDurationMin());
|
||||
}
|
||||
}
|
||||
}
|
||||
initHourPackageUsageFieldsForUpdate(update, before);
|
||||
validEntityBeforeSave(update);
|
||||
|
||||
@@ -21,15 +21,10 @@ public class VideoUploadProperties {
|
||||
private String workDir = System.getProperty("java.io.tmpdir") + File.separator + "hot-video-upload";
|
||||
|
||||
/**
|
||||
* 对外访问的 HLS 路径前缀。
|
||||
* HLS 文件在 OSS 中的对象前缀。
|
||||
*/
|
||||
private String publicUrlPrefix = "/video-hls";
|
||||
|
||||
/**
|
||||
* 对外访问的基础域名,可为空。
|
||||
*/
|
||||
private String publicBaseUrl;
|
||||
|
||||
/**
|
||||
* FFmpeg 可执行命令。
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.system.domain.vo.SysOssVo;
|
||||
import org.dromara.system.service.ISysOssService;
|
||||
import org.redisson.api.RSet;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -25,6 +27,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 视频分片上传、合并与异步转码服务
|
||||
@@ -36,16 +39,14 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
|
||||
private static final String CHUNK_KEY_PREFIX = "video:chunk:";
|
||||
private static final String TASK_KEY_PREFIX = "video:task:";
|
||||
private static final String RESULT_KEY_PREFIX = "video:result:";
|
||||
|
||||
private final VideoUploadProperties properties;
|
||||
private final ISysOssService ossService;
|
||||
|
||||
@Override
|
||||
public List<Integer> checkUploadedChunks(String fileHash) {
|
||||
RSet<Integer> rSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + fileHash);
|
||||
Set<Integer> uploaded = rSet.readAll();
|
||||
List<Integer> result = new ArrayList<>(uploaded);
|
||||
result.sort(Integer::compareTo);
|
||||
return result;
|
||||
return getUploadedChunkIndexes(fileHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,8 +68,15 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
@Override
|
||||
public Map<String, String> mergeAndCreateTask(VideoMergeBo bo) {
|
||||
validateMergeRequest(bo);
|
||||
RSet<Integer> uploadedSet = RedisUtils.getClient().getSet(CHUNK_KEY_PREFIX + bo.getFileHash());
|
||||
Set<Integer> uploaded = uploadedSet.readAll();
|
||||
validateFfmpegAvailability();
|
||||
|
||||
String cachedM3u8Url = getResolvedM3u8Url(bo.getFileHash());
|
||||
if (StringUtils.isNotBlank(cachedM3u8Url)) {
|
||||
return Collections.singletonMap("taskId", saveCompletedTask(bo.getFileHash(), bo.getFileName(), cachedM3u8Url));
|
||||
}
|
||||
|
||||
List<Integer> uploadedChunkIndexes = getUploadedChunkIndexes(bo.getFileHash());
|
||||
Set<Integer> uploaded = new HashSet<>(uploadedChunkIndexes);
|
||||
for (int i = 0; i < bo.getTotalChunks(); i++) {
|
||||
if (!uploaded.contains(i)) {
|
||||
throw new ServiceException("存在未上传完成的分片,无法合并");
|
||||
@@ -99,6 +107,13 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
if (taskInfo == null) {
|
||||
throw new ServiceException("任务不存在或已过期");
|
||||
}
|
||||
if ("completed".equals(taskInfo.getStatus()) && StringUtils.isNotBlank(taskInfo.getFileHash())) {
|
||||
String resolvedM3u8Url = getResolvedM3u8Url(taskInfo.getFileHash());
|
||||
if (StringUtils.isNotBlank(resolvedM3u8Url) && !StringUtils.equals(taskInfo.getM3u8Url(), resolvedM3u8Url)) {
|
||||
taskInfo.setM3u8Url(resolvedM3u8Url).setUpdateTime(LocalDateTime.now());
|
||||
saveTask(taskInfo);
|
||||
}
|
||||
}
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
@@ -115,14 +130,18 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
|
||||
Path playlistPath = outputDir.resolve("index.m3u8");
|
||||
runFfmpeg(sourceFile, outputDir, playlistPath);
|
||||
String m3u8Url = uploadHlsDirectoryToOss(fileHash, outputDir);
|
||||
|
||||
taskInfo.setStatus("completed")
|
||||
.setM3u8Url(buildPublicM3u8Url(fileHash))
|
||||
.setM3u8Url(m3u8Url)
|
||||
.setUpdateTime(LocalDateTime.now())
|
||||
.setErrorMessage(null);
|
||||
saveTask(taskInfo);
|
||||
RedisUtils.setCacheObject(RESULT_KEY_PREFIX + fileHash, m3u8Url, properties.taskExpireDuration());
|
||||
|
||||
FileUtil.del(getChunkDir(fileHash));
|
||||
FileUtil.del(getSourceDir(fileHash));
|
||||
FileUtil.del(outputDir.toFile());
|
||||
log.info("视频转码完成: taskId={}, fileHash={}, playlist={}", taskId, fileHash, playlistPath);
|
||||
} catch (Exception e) {
|
||||
log.error("视频转码失败: taskId={}, fileHash={}, msg={}", taskId, fileHash, e.getMessage(), e);
|
||||
@@ -151,6 +170,33 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFfmpegAvailability() {
|
||||
String ffmpegCommand = StringUtils.defaultIfBlank(properties.getFfmpegCommand(), "ffmpeg").trim();
|
||||
Process process = null;
|
||||
try {
|
||||
process = new ProcessBuilder(ffmpegCommand, "-version")
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
throw new ServiceException("FFmpeg 校验超时,请检查命令配置或容器环境");
|
||||
}
|
||||
String output;
|
||||
try (InputStream inputStream = process.getInputStream()) {
|
||||
output = new String(inputStream.readAllBytes());
|
||||
}
|
||||
if (process.exitValue() != 0) {
|
||||
throw new ServiceException("FFmpeg 不可用: " + abbreviate(output, 300));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("FFmpeg 不可用,请检查命令配置: " + ffmpegCommand);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new ServiceException("FFmpeg 校验被中断");
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeChunksToFile(String fileHash, int totalChunks, Path targetFile) {
|
||||
try {
|
||||
Files.deleteIfExists(targetFile);
|
||||
@@ -167,6 +213,7 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new ServiceException("合并分片失败");
|
||||
}
|
||||
}
|
||||
@@ -210,16 +257,120 @@ public class VideoChunkUploadServiceImpl implements VideoChunkUploadService {
|
||||
RedisUtils.setCacheObject(TASK_KEY_PREFIX + taskInfo.getTaskId(), taskInfo, properties.taskExpireDuration());
|
||||
}
|
||||
|
||||
private String buildPublicM3u8Url(String fileHash) {
|
||||
String relativePath = properties.normalizedPublicUrlPrefix() + "/" + fileHash + "/index.m3u8";
|
||||
if (StringUtils.isBlank(properties.getPublicBaseUrl())) {
|
||||
return relativePath;
|
||||
private List<Integer> getUploadedChunkIndexes(String fileHash) {
|
||||
File chunkDir = getChunkDir(fileHash);
|
||||
if (!chunkDir.exists() || !chunkDir.isDirectory()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String baseUrl = properties.getPublicBaseUrl().trim();
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
File[] chunkFiles = chunkDir.listFiles();
|
||||
if (chunkFiles == null || chunkFiles.length == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return baseUrl + relativePath;
|
||||
List<Integer> result = new ArrayList<>();
|
||||
for (File chunkFile : chunkFiles) {
|
||||
if (chunkFile == null || !chunkFile.isFile()) {
|
||||
continue;
|
||||
}
|
||||
String fileName = chunkFile.getName();
|
||||
if (!fileName.matches("\\d+")) {
|
||||
continue;
|
||||
}
|
||||
result.add(Integer.parseInt(fileName));
|
||||
}
|
||||
result.sort(Integer::compareTo);
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getResolvedM3u8Url(String fileHash) {
|
||||
String cachedM3u8Url = RedisUtils.getCacheObject(RESULT_KEY_PREFIX + fileHash);
|
||||
if (isOssUrl(cachedM3u8Url)) {
|
||||
return cachedM3u8Url;
|
||||
}
|
||||
|
||||
Path outputDir = getHlsDir(fileHash).toPath();
|
||||
Path playlistPath = outputDir.resolve("index.m3u8");
|
||||
if (Files.exists(playlistPath)) {
|
||||
try {
|
||||
String uploadedM3u8Url = uploadHlsDirectoryToOss(fileHash, outputDir);
|
||||
RedisUtils.setCacheObject(RESULT_KEY_PREFIX + fileHash, uploadedM3u8Url, properties.taskExpireDuration());
|
||||
return uploadedM3u8Url;
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("上传 HLS 到 OSS 失败");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String saveCompletedTask(String fileHash, String fileName, String m3u8Url) {
|
||||
String taskId = "task_" + IdUtil.fastSimpleUUID();
|
||||
VideoTranscodeTaskInfo taskInfo = new VideoTranscodeTaskInfo()
|
||||
.setTaskId(taskId)
|
||||
.setFileHash(fileHash)
|
||||
.setFileName(fileName)
|
||||
.setStatus("completed")
|
||||
.setM3u8Url(m3u8Url)
|
||||
.setCreateTime(LocalDateTime.now())
|
||||
.setUpdateTime(LocalDateTime.now());
|
||||
saveTask(taskInfo);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private String uploadHlsDirectoryToOss(String fileHash, Path outputDir) throws IOException {
|
||||
if (!Files.exists(outputDir)) {
|
||||
throw new ServiceException("HLS 输出目录不存在");
|
||||
}
|
||||
List<Path> files;
|
||||
try (var stream = Files.list(outputDir)) {
|
||||
files = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.sorted(Comparator.comparing(path -> path.getFileName().toString()))
|
||||
.toList();
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
throw new ServiceException("未找到待上传的 HLS 文件");
|
||||
}
|
||||
|
||||
Path playlistPath = outputDir.resolve("index.m3u8");
|
||||
if (!Files.exists(playlistPath)) {
|
||||
throw new ServiceException("未生成 index.m3u8");
|
||||
}
|
||||
|
||||
for (Path path : files) {
|
||||
if (path.equals(playlistPath)) {
|
||||
continue;
|
||||
}
|
||||
ossService.upload(path.toFile(), buildOssObjectKey(fileHash, path.getFileName().toString()), resolveContentType(path.getFileName().toString()));
|
||||
}
|
||||
|
||||
SysOssVo playlistOss = ossService.upload(
|
||||
playlistPath.toFile(),
|
||||
buildOssObjectKey(fileHash, playlistPath.getFileName().toString()),
|
||||
resolveContentType(playlistPath.getFileName().toString())
|
||||
);
|
||||
return playlistOss.getUrl();
|
||||
}
|
||||
|
||||
private boolean isOssUrl(String url) {
|
||||
return StringUtils.isNotBlank(url) && (url.startsWith("http://") || url.startsWith("https://"));
|
||||
}
|
||||
|
||||
private String buildOssObjectKey(String fileHash, String fileName) {
|
||||
String prefix = properties.normalizedPublicUrlPrefix();
|
||||
if (prefix.startsWith("/")) {
|
||||
prefix = prefix.substring(1);
|
||||
}
|
||||
return prefix + "/" + fileHash + "/" + fileName;
|
||||
}
|
||||
|
||||
private String resolveContentType(String fileName) {
|
||||
String lowerFileName = fileName == null ? "" : fileName.toLowerCase(Locale.ROOT);
|
||||
if (lowerFileName.endsWith(".m3u8")) {
|
||||
return "application/vnd.apple.mpegurl";
|
||||
}
|
||||
if (lowerFileName.endsWith(".ts")) {
|
||||
return "video/mp2t";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private void cleanDirectory(Path directory) throws IOException {
|
||||
|
||||
@@ -47,6 +47,7 @@ public class SysOssController extends BaseController {
|
||||
*/
|
||||
@SaCheckPermission("system:oss:upload")
|
||||
@GetMapping("/checkChunk")
|
||||
@Deprecated
|
||||
public R<SysOssCheckVo> checkChunk(String identifier) {
|
||||
return R.ok(ossService.checkChunk(identifier));
|
||||
}
|
||||
@@ -56,6 +57,7 @@ public class SysOssController extends BaseController {
|
||||
*/
|
||||
@SaCheckPermission("system:oss:upload")
|
||||
@PostMapping(value = "/uploadChunk", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Deprecated
|
||||
public R<Void> uploadChunk(@Validated SysOssChunkBo bo) {
|
||||
ossService.uploadChunk(bo);
|
||||
return R.ok();
|
||||
@@ -66,6 +68,7 @@ public class SysOssController extends BaseController {
|
||||
*/
|
||||
@SaCheckPermission("system:oss:upload")
|
||||
@PostMapping("/mergeChunk")
|
||||
@Deprecated
|
||||
public R<SysOssVo> mergeChunk(@Validated SysOssMergeBo bo) {
|
||||
return R.ok(ossService.mergeChunk(bo));
|
||||
}
|
||||
|
||||
@@ -86,6 +86,16 @@ public interface ISysOssService {
|
||||
*/
|
||||
SysOssVo upload(File file);
|
||||
|
||||
/**
|
||||
* 按指定对象键上传文件到对象存储服务,并保存文件信息到数据库
|
||||
*
|
||||
* @param file 文件
|
||||
* @param objectKey OSS 对象键
|
||||
* @param contentType 内容类型
|
||||
* @return 上传成功后的 SysOssVo 对象
|
||||
*/
|
||||
SysOssVo upload(File file, String objectKey, String contentType);
|
||||
|
||||
/**
|
||||
* 文件下载方法,支持一次性下载完整文件
|
||||
*
|
||||
|
||||
@@ -353,6 +353,36 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult, ext1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysOssVo upload(File file, String objectKey, String contentType) {
|
||||
String originalfileName = file.getName();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance();
|
||||
|
||||
SysOssExt ext1 = new SysOssExt();
|
||||
ext1.setFileSize(file.length());
|
||||
ext1.setContentType(contentType);
|
||||
|
||||
Long duration = null;
|
||||
if (isMediaFile(suffix)) {
|
||||
if (file.length() > MEDIA_DURATION_PARSE_MAX_SIZE) {
|
||||
log.info("文件较大,跳过媒体时长解析: name={}, size={} bytes", originalfileName, file.length());
|
||||
} else {
|
||||
try {
|
||||
duration = getMediaDuration(file);
|
||||
} catch (Exception e) {
|
||||
log.warn("解析媒体时长失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String resolvedContentType = StringUtils.isNotBlank(contentType) ? contentType : FileUtils.getMimeType(suffix);
|
||||
UploadResult uploadResult = storage.upload(file.toPath(), objectKey, null, resolvedContentType);
|
||||
|
||||
ext1.setDuration(duration);
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult, ext1);
|
||||
}
|
||||
|
||||
private boolean isMediaFile(String suffix) {
|
||||
if (StringUtils.isBlank(suffix)) return false;
|
||||
String s = suffix.toLowerCase().replace(".", "");
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<select id="selectWrongQuestionReviewList"
|
||||
resultType="com.hotwj.platform.securityManagement.trainingAnswer.domain.vo.HotTrainingWrongQuestionReviewVo">
|
||||
SELECT ar.question_id AS questionId,
|
||||
count(ar.question_id) AS errorCount
|
||||
SUM(ar.error_count) AS errorCount
|
||||
FROM hot_training_answer_record ar
|
||||
WHERE ar.is_deleted = 0
|
||||
AND ar.company_id = #{companyId}
|
||||
@@ -148,8 +148,7 @@
|
||||
AND ar.scene_type = 'PLAN'
|
||||
AND ar.training_id = #{bo.trainingId}
|
||||
AND ar.exam_id = #{bo.examId}
|
||||
AND ar.error_answers IS NOT NULL
|
||||
AND ar.error_answers <> ''
|
||||
AND ar.error_count > 0
|
||||
GROUP BY ar.question_id
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user