This commit is contained in:
2025-02-25 14:42:48 +08:00
parent f957c6aaf7
commit 0d0a7825b4
39 changed files with 4430 additions and 21 deletions
@@ -0,0 +1,146 @@
package io.v.nutz.zhgh.qsv.controller;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.base.query.PageForm;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvOption;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.service.QsvActivityService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
@IocBean
@At("/platform/qsv/activity")
@Ok("json:full")
public class QsvActivityController {
@Inject
private Dao dao;
@Inject
private QsvActivityService qsvActivityService;
@At("")
@Ok("beetl:/platform/qsv/activity/index.html")
@RequiresPermissions("qsv.activity")
public void index() {
}
// 分页查询
@At
@RequiresPermissions("qsv.activity")
public Result pageData(@Valid PageForm pageForm, Integer year, String title) {
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
cnd.and(Cnd.likeEX("title",title));
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
// 保存问卷基础信息
@At
@RequiresPermissions("qsv.activity")
public Result save(QsvActivity qsvActivity) {
if (qsvActivity.getCategory().equals("QUIZ")) {
if (qsvActivity.getMode().equals("SCHEDULED")) {
qsvActivity.setRepeatMode("DAILY");
} else if (qsvActivity.getMode().equals("REGULAR")) {
qsvActivity.setRepeatMode("TOTAL");
}
}
dao.insertOrUpdate(qsvActivity);
return Result.success();
}
@At
@RequiresPermissions("qsv.activity")
public Result findOne(@Valid String id) {
QsvActivity activity = dao.fetch(QsvActivity.class, id);
return Result.success(activity);
}
// 删除问卷
@At
@RequiresPermissions("qsv.activity")
public Result delete(@Valid String id) {
dao.delete(QsvActivity.class, id);
return Result.success();
}
// 保存问卷题目
@At
@RequiresPermissions("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
public Result saveSubjects(@Param("activityId") @Valid String activityId, @Param("subjects") QsvSubject[] qsvSubjects) {
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).toList();
//新题目
List<String> newSubjectIds = Arrays.stream(qsvSubjects).map(QsvSubject::getId).toList();
//过滤出需要删除的题目
List<String> deleteSubjectIds = subjectIds.stream().filter(id -> !newSubjectIds.contains(id)).toList();
dao.clear(QsvSubject.class, Cnd.where("id", "in", deleteSubjectIds));
for (int i = 0; i < qsvSubjects.length; i++) {
QsvSubject qsvSubject = qsvSubjects[i];
qsvSubject.setSortNum(i + 1);
qsvSubject.setActivityId(activityId);
//更新或添加题目
dao.insertOrUpdate(qsvSubject);
//更新或添加选项
List<QsvOption> newOptions = qsvSubject.getOptions();
List<String> optionIds = newOptions.stream().map(QsvOption::getId).toList();
List<QsvOption> oldOptions = dao.query(QsvOption.class, Cnd.where("subjectId", "=", qsvSubject.getId()));
List<String> oldOptionIds = oldOptions.stream().map(QsvOption::getId).toList();
List<String> deleteOptionIds = oldOptionIds.stream().filter(id -> !optionIds.contains(id)).toList();
dao.clear(QsvOption.class, Cnd.where("id", "in", deleteOptionIds));
if (qsvSubject.getType().equals("radio") || qsvSubject.getType().equals("checkbox")) {
for (int i1 = 0; i1 < newOptions.size(); i1++) {
newOptions.get(i1).setSortNum(i1 + 1);
newOptions.get(i1).setSubjectId(qsvSubject.getId());
}
dao.insertOrUpdate(newOptions);
List<String> correctOptionIds = newOptions.stream().filter(QsvOption::getIsCorrect).map(QsvOption::getId).toList();
qsvSubject.setCorrectAnswer(correctOptionIds);
} else {
dao.clear(QsvOption.class, Cnd.where("subjectId", "=", qsvSubject.getId()));
}
dao.update(qsvSubject);
}
return Result.success();
}
// 查询问卷题目
@At
@RequiresPermissions("qsv.activity")
public Result listSubjects(@Valid String activityId) {
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
dao.fetchLinks(subjects, "options",Cnd.NEW().asc("sortNum"));
return Result.success(subjects);
}
}
@@ -0,0 +1,63 @@
package io.v.nutz.zhgh.qsv.controller;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.param.QsvQuizRankPageForm;
import io.v.nutz.zhgh.qsv.service.QsvQuizRankService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
@IocBean
@At("/platform/qsv/quizRank")
@Ok("json:full")
public class QsvQuizRankController {
@Inject
private Dao dao;
@Inject
private QsvQuizRankService qsvQuizRankService;
@At("")
@Ok("beetl:/platform/qsv/quiz/rank.html")
@RequiresPermissions("qsv.quiz.rank")
public void index() {
}
// 根据年度查询问卷
@At
@RequiresPermissions("qsv.quiz.rank")
public Result listQuiz(Integer year) {
Cnd cnd = Cnd.where("category", "=", "QUIZ");
cnd.andEX("YEAR(startTime)", "=", year);
cnd.desc("category");
List<QsvActivity> list = dao.query(QsvActivity.class, cnd);
return Result.success(list);
}
@At
@RequiresPermissions("qsv.quiz.rank")
public Result pageData(@Valid QsvQuizRankPageForm pageForm) {
Pagination pagination = qsvQuizRankService.pageData(pageForm);
return Result.success(pagination);
}
@At
@Ok("void")
@RequiresPermissions("qsv.quiz.rank")
public void exportXlsx(@Valid QsvQuizRankPageForm pageForm, HttpServletResponse response) {
qsvQuizRankService.exportXlsx(pageForm, response);
}
}
@@ -0,0 +1,98 @@
package io.v.nutz.zhgh.qsv.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.base.query.PageForm;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.service.QsvActivityService;
import io.v.nutz.zhgh.qsv.service.QsvSurveyService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.List;
@IocBean
@At("/platform/qsv/survey")
@Ok("json:full")
public class QsvSurveyController {
@Inject
private Dao dao;
@Inject
private QsvActivityService qsvActivityService;
@Inject
private QsvSurveyService qsvSurveyService;
@At("")
@Ok("beetl:/platform/qsv/survey/index.html")
@RequiresPermissions("qsv.survey")
public void index() {
}
@At
@RequiresPermissions("qsv.survey")
public Result pageData(@Valid PageForm pageForm, Integer year) {
Cnd cnd = Cnd.where("category", "=", "SURVEY");
cnd.andEX("YEAR(startTime)", "=", year);
cnd.desc("category");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
// 报告
@At
@RequiresPermissions("qsv.survey")
public Result report(@Valid String activityId) {
List<NutMap> report = qsvSurveyService.report(activityId);
return Result.success(report);
}
// 选项选择详情
@At
@RequiresPermissions("qsv.survey")
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) {
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
List<QsvUserAnswerRecord> selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList();
return Result.success(selectOptionUsers);
}
// 用户答题
@At
@RequiresPermissions("qsv.survey")
public Result userAnswer(@Valid String activityId) {
NutMap map = qsvSurveyService.userAnswer(activityId);
return Result.success(map);
}
// 删除用户答题记录
@At
@RequiresPermissions("qsv.survey")
public Result deleteUserAnswer(@Valid String id) {
dao.delete(QsvUserAnswerRecord.class, id);
return Result.success();
}
// 导出用户答题记录xlsx
@At
@RequiresPermissions("qsv.survey")
public void exportUserAnswerXlsx(@Valid String activityId, HttpServletResponse response) {
qsvSurveyService.exportUserAnswerXlsx(activityId, response);
}
}
@@ -0,0 +1,17 @@
package io.v.nutz.zhgh.qsv.dto;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
//答题得分计算结果
@Data
@Accessors(chain = true)
@Builder
public class QsvCheckAnswerResult {
private boolean isCorrect;
private float score;
}
@@ -0,0 +1,45 @@
package io.v.nutz.zhgh.qsv.h5controller;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.base.query.PageForm;
import io.v.nutz.zhgh.qsv.service.QsvActivityService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.dao.Cnd;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.validation.Valid;
@IocBean
@At("/platform/h5/qsv")
@Ok("json:full")
@Slf4j
public class H5QsvController {
@Inject
private QsvActivityService qsvActivityService;
@At("")
@Ok("beetl:/mobile/qsv/index.html")
@RequiresPermissions("h5.qsv")
public void index() {
}
// 分页查询
@At
@RequiresPermissions("h5.qsv")
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
Cnd cnd = Cnd.NEW();
cnd.andEX("category", "=", category);
cnd.desc("startTime");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
}
}
@@ -0,0 +1,372 @@
package io.v.nutz.zhgh.qsv.h5controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.wizzer.framework.base.Result;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.qsv.dto.QsvCheckAnswerResult;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvAnswerParam;
import io.v.nutz.zhgh.qsv.service.QsvQuizService;
import io.v.nutz.zhgh.qsv.service.QsvUserAnswerRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.*;
import java.util.stream.Collectors;
@IocBean
@At("/platform/h5/qsv/quiz")
@Ok("json:full")
@Slf4j
public class H5QsvQuizController {
@Inject
private Dao dao;
@Inject
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
@Inject
private QsvQuizService qsvQuizService;
@At("")
@Ok("beetl:/mobile/qsv/quiz/index.html")
@RequiresAuthentication
public void index() {
}
@At
@Ok("beetl:/mobile/qsv/quiz/result.html")
@RequiresAuthentication
public void result() {
}
// 题目列表
@At
@RequiresAuthentication
public Result subjects(@Valid String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
String mode = activity.getMode();
//能否重复答题
Boolean repeatable = activity.getRepeatable();
//重复答题模式
String repeatMode = activity.getRepeatMode();
//答题最大次数
Integer maxAttempts = activity.getMaxAttempts();
//题目显示模式
String displayMode = activity.getDisplayMode();
//最终返回的题目数据
List<QsvSubject> resultSubjects = new ArrayList<>();
String answerRecordId = null;
//查询用户的答题记录
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId()));
if (activity.getEndTime().after(new Date())) {
//已结束 查询最新一次的答题记录
Optional<QsvUserAnswerRecord> lastRecordOptional = answerRecords.stream().max(Comparator.comparing(QsvUserAnswerRecord::getAnswerTime));
if (lastRecordOptional.isPresent()) {
List<String> subjectIds = lastRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = lastRecordOptional.get().getId();
}else{
//没生成过 那就看全部的题目
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
answerRecordId = answerRecord.getId();
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
} else {
if (mode.equals("REGULAR")) {
if (ObjectUtil.isEmpty(answerRecords)) {
//首次进来生成答题记录
if (displayMode.equals("ALL")) {
resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
} else if (displayMode.equals("RANDOM")) {
//单次随机抽取题目数量
Integer randomCount = activity.getRandomCount();
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
if (subjects.size() < randomCount) {
throw new RuntimeException("题目数不够,无法生成题目");
}
//从subjects里随机抽取randomCount个题目
Collections.shuffle(subjects);
List<QsvSubject> randomSubjects = subjects.subList(0, randomCount);
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, randomSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
answerRecordId = answerRecord.getId();
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
} else {
if (displayMode.equals("ALL")) {
//判断能否重复答题 如果可重复要根据次数判断是否再次生成记录 不能重复直接返回最新的一次记录
if (repeatable) {
//已回答次数
if (answerRecords.size() < maxAttempts) {
//生成答题记录
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
} else {
//已达到最大次数 返回最新一次记录
Optional<QsvUserAnswerRecord> maxAnswerRecordOptional = answerRecords.stream().max(Comparator.comparingLong(QsvUserAnswerRecord::getRandomNumber));
if (maxAnswerRecordOptional.isPresent()) {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
throw new RuntimeException("业务异常");
}
}
} else {
Optional<QsvUserAnswerRecord> maxAnswerRecordOptional = answerRecords.stream().max(Comparator.comparingLong(QsvUserAnswerRecord::getRandomNumber));
if (maxAnswerRecordOptional.isPresent()) {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
throw new RuntimeException("业务异常");
}
}
} else if (displayMode.equals("RANDOM")) {
//判断是否有未答完的记录
Optional<QsvUserAnswerRecord> notFinishRecordOptional = answerRecords.stream().filter(r -> !r.getIsFinish()).findFirst();
if (notFinishRecordOptional.isPresent()) {
List<String> subjectIds = notFinishRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = notFinishRecordOptional.get().getId();
} else {
//随机抽取模式
//单次随机抽取题目数量
Integer randomCount = activity.getRandomCount();
//总随机抽取次数
Integer totalRandom = activity.getTotalRandom();
Date startTime = activity.getStartTime();
Date endTime = activity.getEndTime();
//是否同一天
boolean isSameDay = DateUtil.isSameDay(startTime, endTime);
// if(!isSameDay){
if (answerRecords.size() < totalRandom) {
//进行下一次抽取
List<String> subjectIds = answerRecords.stream().map(QsvUserAnswerRecord::getSubjectIds).flatMap(Collection::stream).toList();
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId)
.and("id", "not in", subjectIds));
if (subjects.size() < randomCount) {
throw new RuntimeException("题目数不够,无法生成题目");
}
//从subjects里随机抽取randomCount个题目
Collections.shuffle(subjects);
resultSubjects = subjects.subList(0, randomCount);
answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId();
} else {
//返回最后一次抽取的记录
Optional<QsvUserAnswerRecord> maxAnswerRecordOptional = answerRecords.stream().max(Comparator.comparingLong(QsvUserAnswerRecord::getRandomNumber));
if (maxAnswerRecordOptional.isPresent()) {
List<String> subjectIds = maxAnswerRecordOptional.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
answerRecordId = maxAnswerRecordOptional.get().getId();
} else {
throw new RuntimeException("业务异常");
}
}
// }else{
//
// }
}
}
}
} else if (mode.equals("SCHEDULED")) {
//定时定题
//查询今天的题目
List<QsvUserAnswerRecord> todayRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId())
.and("attemptDate", "=", DateUtil.today())
);
if (ObjectUtil.isNotEmpty(todayRecords)) {
//判断下每天可以答几次(题目实际上都是一样的)
boolean todayAllFinish = todayRecords.stream().allMatch(QsvUserAnswerRecord::getIsFinish);
//如果今天已生成的题目已答完并且还可以重复答
if (todayAllFinish && todayRecords.size() < maxAttempts) {
//生成今天的记录
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId)
.and("displayDate", "=", DateUtil.today()));
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
answerRecordId = answerRecord.getId();
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
} else {
//今天最大那次的记录
Optional<QsvUserAnswerRecord> maxTodayRecord = todayRecords.stream().max(Comparator.comparingLong(QsvUserAnswerRecord::getRandomNumber));
if (maxTodayRecord.isPresent()) {
answerRecordId = maxTodayRecord.get().getId();
List<String> subjectIds = maxTodayRecord.get().getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
} else {
throw new RuntimeException("业务异常");
}
}
} else {
//生成今天的记录
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId)
.and("displayDate", "=", DateUtil.today()));
QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).collect(Collectors.toList()));
answerRecordId = answerRecord.getId();
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
resultSubjects = dao.query(QsvSubject.class, cnd);
}
}
}
dao.fetchLinks(resultSubjects, "options", Cnd.NEW().asc("sortNum"));
resultSubjects.forEach(v -> v.setUserSelectOptionIds(new ArrayList<>()));
NutMap result = NutMap.NEW().addv("subjects", resultSubjects).addv("answerRecordId", answerRecordId).addv("activity", activity);
return Result.success(result);
}
// 答题记录
@At
@RequiresAuthentication
public Result answerRecord(@Valid String answerRecordId) {
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
return Result.success(record);
}
// 提交答题
@At
@RequiresAuthentication
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
JSONObject extJson = answerRecord.getExtJson();
float totalScore = 0;
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
if (ObjectUtil.isNotEmpty(entries)) {
entries.set("optionIds", subject.getUserSelectOptionIds());
entries.set("text", subject.getUserFillContent());
QsvCheckAnswerResult answerResult = qsvQuizService.calcScore(subject.getId(), subject.getUserSelectOptionIds());
entries.set("score", answerResult.getScore());
entries.set("isCorrect", answerResult.isCorrect());
if (answerResult.isCorrect()) {
totalScore += answerResult.getScore();
}
}
extJson.set(subject.getId(), entries);
}
answerRecord.setTotalScore(totalScore);
answerRecord.setAttemptDate(new Date());
answerRecord.setSubmitTime(new Date());
answerRecord.setAnswerTime(qsvAnswerParam.getAnswerTime());
answerRecord.setIsFinish(true);
dao.update(answerRecord);
//重新计算最高分 最新得分
qsvUserAnswerRecordService.calcByScoreMode(qsvAnswerParam.getActivityId(), ShiroUtil.getUserId());
return Result.success("提交成功");
}
@At
@RequiresAuthentication
public Result historyScore(@Valid String activityId) {
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId).and("userId", "=", ShiroUtil.getUserId()).desc("createdAt"));
return Result.success(answerRecords);
}
// 答题记录(结果页展示)
@At
@RequiresAuthentication
public Result answerResult(@Valid String answerRecordId) {
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
List<String> subjectIds = answerRecord.getSubjectIds();
Cnd cnd = Cnd.where("id", "in", subjectIds);
String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(","));
cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")"));
List<QsvSubject> subjects = dao.query(QsvSubject.class, cnd);
dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum"));
subjects.forEach(v -> v.setUserSelectOptionIds(new ArrayList<>()));
QsvActivity activity = dao.fetch(QsvActivity.class, answerRecord.getActivityId());
NutMap result = NutMap.NEW().addv("subjects", subjects).addv("answerRecordId", answerRecord.getId()).addv("activity", activity);
return Result.success(result);
}
}
@@ -0,0 +1,106 @@
package io.v.nutz.zhgh.qsv.h5controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.wizzer.framework.base.Result;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvAnswerParam;
import io.v.nutz.zhgh.qsv.service.QsvUserAnswerRecordService;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@IocBean
@At("/platform/h5/qsv/survey")
@Ok("json:full")
public class H5QsvSurveyController {
@Inject
private Dao dao;
@Inject
private QsvUserAnswerRecordService qsvUserAnswerRecordService;
@At("")
@Ok("beetl:/mobile/qsv/survey/index.html")
@RequiresAuthentication
public void index() {
}
@At
@RequiresAuthentication
public Result subjects(@Valid String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
String answerRecordId = null;
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId()));
if (answerRecord == null) {
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList());
}
QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class,
Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId()));
answerRecordId = answerRecord2.getId();
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()));
dao.fetchLinks(subjects, "options");
for (QsvSubject subject : subjects) {
if (subject.getUserSelectOptionIds() == null) {
subject.setUserSelectOptionIds(new ArrayList<>());
}
}
NutMap result = NutMap.NEW().addv("subjects", subjects).addv("answerRecordId", answerRecordId).addv("activity", activity);
return Result.success(result);
}
@At
@RequiresAuthentication
public Result answerRecord(@Valid String answerRecordId) {
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
return Result.success(record);
}
@At
@RequiresAuthentication
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
JSONObject extJson = answerRecord.getExtJson();
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
JSONObject entries = extJson.get(subject.getId(), JSONObject.class);
if (ObjectUtil.isNotEmpty(entries)) {
entries.set("optionIds", subject.getUserSelectOptionIds());
entries.set("text", subject.getUserFillContent());
}
extJson.set(subject.getId(), entries);
}
answerRecord.setAttemptDate(new Date());
answerRecord.setSubmitTime(new Date());
answerRecord.setIsFinish(true);
dao.update(answerRecord);
return Result.success("提交成功");
}
}
@@ -0,0 +1,109 @@
package io.v.nutz.zhgh.qsv.models;
import io.v.nutz.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@Table("qsv_activity")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票活动表")
public class QsvActivity extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("活动标题")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String title;
@Column
@Comment("活动描述")
@ColDefine(type = ColType.TEXT)
private String description;
@Column
@Comment("所属模块(quiz, survey, vote)")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String category;
@Column
@Comment("活动开始时间")
@ColDefine(type = ColType.DATETIME)
private Date startTime;
@Column
@Comment("活动结束时间")
@ColDefine(type = ColType.DATETIME)
private Date endTime;
@Column
@Comment("活动分组ID")
@ColDefine(type = ColType.INT)
private Integer groupId;
@Column
@Comment("模式:定时定题模式(scheduled)或常规模式(regular")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String mode;
@Column
@Comment("是否可重复答题")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean repeatable;
@Column
@Comment("重复模式:按天(daily)或按活动总次数(total)")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String repeatMode;
@Column
@Comment("最大答题次数(0表示不限制)")
@ColDefine(type = ColType.INT, width = 1)
private Integer maxAttempts;
@Column
@Comment("显示模式:全部显示、随机抽取显示(ALL、RANDOM)")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String displayMode;
@Column
@Comment("单次随机抽取题目数量")
@ColDefine(type = ColType.INT, width = 1)
private Integer randomCount;
@Column
@Comment("总随机抽取次数")
@ColDefine(type = ColType.INT, width = 1)
private Integer totalRandom;
@Column
@Comment("随机打乱题目顺序")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean shuffleSubject;
@Column
@Comment("答题时间限制(以分钟为单位,0表示无限制)")
@ColDefine(type = ColType.INT, width = 1)
private Integer timeLimit;
@Column
@Comment("答题得分统计(最高、最新)")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String scoreMode;
@Column
@Comment("封面图片")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String cover;
}
@@ -0,0 +1,49 @@
package io.v.nutz.zhgh.qsv.models;
import io.v.nutz.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
@EqualsAndHashCode(callSuper = true)
@Data
@Table("qsv_option")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票选项表")
public class QsvOption extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("题目表")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String subjectId;
@Column
@Comment("选项内容")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String text;
@Column
@Comment("是否为正确答案(仅答题模块使用)")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isCorrect;
@Column
@Comment("图片地址")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String imgUrl;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT)
private Integer sortNum;
}
@@ -0,0 +1,76 @@
package io.v.nutz.zhgh.qsv.models;
import io.v.nutz.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("qsv_subject")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票题目表")
public class QsvSubject extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("活动ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String activityId;
@Column
@Comment("题目标题")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String title;
@Column
@Comment("题目类型(single, multi, judge, fill")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String type;
@Column
@Comment("正确答案")
@ColDefine(type = ColType.MYSQL_JSON)
private List<String> correctAnswer; // 正确答案
@Column
@Comment("题目分数(仅答题模块使用)")
@ColDefine(type = ColType.INT, width = 1)
private Integer score; //
@Column
@Comment("显示时间")
@ColDefine(type = ColType.DATE)
private Date displayDate;
@Column
@Comment("排序")
@ColDefine(type = ColType.INT, width = 1)
private Integer sortNum;
/**
* 选项
*/
@Many(target = QsvOption.class, field = "subjectId")
private List<QsvOption> options;
/**
* 用户选择的选项(选择题)
*/
private List<String> userSelectOptionIds;
/**
* 用户填写的内容(填空题)
*/
private String userFillContent;
}
@@ -0,0 +1,124 @@
package io.v.nutz.zhgh.qsv.models;
import cn.hutool.json.JSONObject;
import io.v.nutz.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.util.Date;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("qsv_user_answer_record")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票记录表")
public class QsvUserAnswerRecord extends BaseModel {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String loginName;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String userName;
@Column
@Comment("分工会")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String unionName;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("单位")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String unitName;
@Column
@Comment("单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("活动ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String activityId;
@Column
@Comment("扩展字段")
@ColDefine(type = ColType.MYSQL_JSON)
private JSONObject extJson;
@Column
@Comment("题目ID")
@ColDefine(type = ColType.MYSQL_JSON)
private List<String> subjectIds;
@Column
@Comment("答题得分(仅答题模块使用)")
@ColDefine(type = ColType.FLOAT)
private Float totalScore;
@Column
@Comment("随机次数(仅答题模块使用随机抽取模式)")
@ColDefine(type = ColType.INT, width = 1)
private Integer randomNumber;
@Column
@Comment("答题次数(仅答题模块使用)")
@ColDefine(type = ColType.INT, width = 1)
private Integer attemptNumber;
@Column
@Comment("答题日期(用于统计某一天的答题次数)")
@ColDefine(type = ColType.DATE)
private Date attemptDate;
@Column
@Comment("是否为最新得分(仅答题模块使用)")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isLatestScore;
@Column
@Comment("是否为最高得分(仅答题模块使用)")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isHighestScore;
@Column
@Comment("是否完成")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean isFinish;
@Column
@Comment("提交时间")
@ColDefine(type = ColType.DATETIME)
private Date submitTime;
@Column
@Comment("答题用时")
@ColDefine(type = ColType.INT)
private Integer answerTime;
}
@@ -0,0 +1,26 @@
package io.v.nutz.zhgh.qsv.param;
import lombok.Data;
import java.util.List;
@Data
//答题参数
public class QsvAnswerParam {
private String activityId;
private String answerRecordId;
private List<Subject> subjects;
private Integer answerTime;
@Data
public static class Subject{
private String id;
private List<String> userSelectOptionIds;
private String userFillContent;
}
}
@@ -0,0 +1,22 @@
package io.v.nutz.zhgh.qsv.param;
import io.v.nutz.base.query.PageForm;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
//问卷排名得分分页查询参数
public class QsvQuizRankPageForm extends PageForm {
private String activityId;
private String unionId;
private String unitId;
private Date attemptDate;
}
@@ -0,0 +1,8 @@
package io.v.nutz.zhgh.qsv.service;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
public interface QsvActivityService extends BaseService<QsvActivity> {
}
@@ -0,0 +1,17 @@
package io.v.nutz.zhgh.qsv.service;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvQuizRankPageForm;
import javax.servlet.http.HttpServletResponse;
public interface QsvQuizRankService extends BaseService<QsvUserAnswerRecord> {
Pagination pageData(QsvQuizRankPageForm pageForm);
void exportXlsx(QsvQuizRankPageForm pageForm, HttpServletResponse response);
}
@@ -0,0 +1,20 @@
package io.v.nutz.zhgh.qsv.service;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.zhgh.qsv.dto.QsvCheckAnswerResult;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import java.util.List;
public interface QsvQuizService extends BaseService<QsvUserAnswerRecord> {
/**
* 计算答题分数
* @param subjectId 题目ID
* @param userSelectOptions 用户选项
* @return 分数
*/
QsvCheckAnswerResult calcScore(String subjectId, List<String> userSelectOptions);
}
@@ -0,0 +1,17 @@
package io.v.nutz.zhgh.qsv.service;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
public interface QsvSurveyService extends BaseService<QsvUserAnswerRecord> {
List<NutMap> report(String activityId);
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
NutMap userAnswer(String activityId);
}
@@ -0,0 +1,21 @@
package io.v.nutz.zhgh.qsv.service;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvAnswerParam;
import java.util.List;
public interface QsvUserAnswerRecordService extends BaseService<QsvUserAnswerRecord> {
QsvUserAnswerRecord insertRecord(String activityId, List<String> subjectIds);
/**
* 计算得分
*/
float calcScore(QsvAnswerParam qsvAnswerParam);
void calcByScoreMode(String activityId, String userId);
}
@@ -0,0 +1,15 @@
package io.v.nutz.zhgh.qsv.service.impl;
import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.service.QsvActivityService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
@IocBean(args = {"refer:dao"})
public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> implements QsvActivityService {
public QsvActivityServiceImpl(Dao dao) {
super(dao);
}
}
@@ -0,0 +1,145 @@
package io.v.nutz.zhgh.qsv.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.StrUtil;
import io.v.nutz.base.page.Pagination;
import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvQuizRankPageForm;
import io.v.nutz.zhgh.qsv.service.QsvQuizRankService;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> implements QsvQuizRankService {
public QsvQuizRankServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination pageData(QsvQuizRankPageForm pageForm) {
String activityId = pageForm.getActivityId();
if(StrUtil.isBlank(activityId)){
return new Pagination();
}
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
if (activity == null) {
return new Pagination();
}
String mode = activity.getMode();
String scoreStatisticsMode = activity.getScoreMode();
Sql sql = buildQuerySql(activityId, mode, scoreStatisticsMode, pageForm);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public void exportXlsx(QsvQuizRankPageForm pageForm, HttpServletResponse response) {
String activityId = pageForm.getActivityId();
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
if (activity == null) {
throw new RuntimeException("活动不存在: " + activityId);
}
String mode = activity.getMode();
String scoreStatisticsMode = activity.getScoreMode();
Sql sql = buildQuerySql(activityId, mode, scoreStatisticsMode, pageForm);
List<NutMap> list = listMap(sql);
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("姓名", "userName", 20));
exportEntities.add(new ExcelExportEntity("工号", "loginName", 20));
exportEntities.add(new ExcelExportEntity("性别", "sex", 20));
exportEntities.add(new ExcelExportEntity("单位", "unitName", 30));
exportEntities.add(new ExcelExportEntity("分工会", "unionName", 30));
exportEntities.add(new ExcelExportEntity("联系方式", "mobile", 30));
if ("SCHEDULED".equals(mode) && pageForm.getAttemptDate() != null) {
exportEntities.add(new ExcelExportEntity("答题日期", "attemptDate", 20));
exportEntities.add(new ExcelExportEntity("当天得分", "totalScore", 20));
} else {
exportEntities.add(new ExcelExportEntity("得分", "sumScore", 20));
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, list);
// CommonDownloadUtil.download(activity.getTitle() + "得分名单" + ".xlsx", workbook, response);
}
private Sql buildQuerySql(String activityId, String mode, String scoreStatisticsMode, QsvQuizRankPageForm pageForm) {
StringBuilder sqlBuilder = new StringBuilder("""
SELECT
t1.userId,
t1.userName,
t1.loginName,
t1.unitName,
t1.unionName,
u.mobile,
t1.totalScore,
t1.attemptDate,
sum(t1.totalScore) as sumScore
FROM
`qsv_user_answer_record` t1
LEFT JOIN sys_user u ON u.id = t1.userId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t1.activityId","=",activityId);
if(StrUtil.isNotBlank(pageForm.getSearchKeyword())){
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("t1.loginName", pageForm.getSearchKeyword());
seg.orLike("t1.userName", pageForm.getSearchKeyword());
cnd.and(seg);
}
cnd.andEX("t1.unionId","=",pageForm.getUnionId());
cnd.andEX("t1.unitId","=",pageForm.getUnitId());
if ("SCHEDULED".equals(mode)) {
if ("HIGH".equals(scoreStatisticsMode)) {
cnd.and("t1.isHighestScore","=",1);
} else if ("LAST".equals(scoreStatisticsMode)) {
cnd.and("t1.isLatestScore","=",1);
}
} else if ("REGULAR".equals(mode)) {
if ("HIGH".equals(scoreStatisticsMode)) {
cnd.and("t1.isHighestScore","=",1);
} else if ("LAST".equals(scoreStatisticsMode)) {
cnd.and("t1.isLatestScore","=",1);
}
}
if (pageForm.getAttemptDate() != null) {
cnd.and("t1.attemptDate","=",pageForm.getAttemptDate());
}
cnd.groupBy("t1.userId");
cnd.desc("t1.totalScore");
Sql sql = Sqls.create(sqlBuilder.toString());
sql.setCondition(cnd);
return sql;
}
}
@@ -0,0 +1,36 @@
package io.v.nutz.zhgh.qsv.service.impl;
import cn.hutool.core.util.ObjectUtil;
import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.zhgh.qsv.dto.QsvCheckAnswerResult;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.service.QsvQuizService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class QsvQuizServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> implements QsvQuizService {
public QsvQuizServiceImpl(Dao dao) {
super(dao);
}
@Override
public QsvCheckAnswerResult calcScore(String subjectId, List<String> userSelectOptions) {
QsvSubject subject = dao().fetch(QsvSubject.class, subjectId);
List<String> correctOptionIds = subject.getCorrectAnswer();
if(ObjectUtil.isEmpty(userSelectOptions)){
userSelectOptions = new ArrayList<>();
}
//比较选项是否正确 不考虑顺序
boolean equals = new HashSet<>(userSelectOptions).equals(new HashSet<>(correctOptionIds));
return QsvCheckAnswerResult.builder().isCorrect(equals).score(equals ? subject.getScore() : 0).build();
}
}
@@ -0,0 +1,258 @@
package io.v.nutz.zhgh.qsv.service.impl;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvOption;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.service.QsvSurveyService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@IocBean(args = {"refer:dao"})
public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> implements QsvSurveyService {
public QsvSurveyServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<NutMap> report(String activityId) {
// 检查活动ID是否为空
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("活动ID不能为空");
}
try {
// 查询用户答题记录
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
// 将答题记录的扩展JSON转换为JSONObject列表
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<NutMap> subjects = querySubjects(activityId);
// 获取题目ID列表
List<String> subjectIds = subjects.stream().map(v -> v.getString("id")).toList();
// 查询题目选项列表
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
// 按题目ID分组选项
Map<String, List<QsvOption>> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
// 处理每个题目
for (NutMap subject : subjects) {
// 获取当前题目的选项列表
List<NutMap> subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class);
// 获取题目类型
String subjectType = subject.getString("type");
// 处理文本类型题目
if ("text".equals(subjectType)) {
List<String> texts = answerExtList.stream()
.filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class)))
.map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text"))
.toList();
subject.put("texts", texts);
}
// 处理单选类型题目 处理多选类型题目
else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) {
subjectOptions.forEach(subjectOption -> {
long selectCount = answerExtList.stream()
.filter(ext ->
{
JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class);
return jsonObject != null
&& jsonObject.getJSONArray("optionIds") != null
&& jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id"));
})
.count();
subjectOption.put("selectCount", selectCount);
});
long selectTotal = answerExtList.stream()
.filter(ext -> ext.containsKey(subject.getString("id")))
.count();
subject.put("selectTotal", selectTotal);
}
// 添加选项到题目中
subject.addv("options", subjectOptions);
}
return subjects;
} catch (Exception e) {
log.error("报告生成失败", e);
throw new RuntimeException("报告生成失败", e);
}
}
@Override
public void exportUserAnswerXlsx(String activityId, HttpServletResponse response) {
// 检查活动ID是否为空
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("活动ID不能为空");
}
try {
// 查询活动信息
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
if (activity == null) {
throw new IllegalArgumentException("活动不存在");
}
// 查询用户答题记录
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
// 将答题记录的扩展JSON转换为JSONObject列表
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).toList();
// 查询题目选项列表
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
// 将选项列表转换为Map
Map<String, QsvOption> optionMap = options.stream().collect(Collectors.toMap(QsvOption::getId, v -> v));
// 构建Excel导出实体
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
// 构建答题记录列表
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
// 设置导出参数
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
// 导出Excel并下载
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, excelExportEntities, list)) {
// CommonDownloadUtil.download(activity.getTitle() + "答题记录" + ".xlsx", workbook, response);
}
} catch (Exception e) {
log.error("导出Excel失败", e);
throw new RuntimeException("导出Excel失败", e);
}
}
@Override
public NutMap userAnswer(String activityId) {
// 检查活动ID是否为空
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("活动ID不能为空");
}
try {
// 查询用户答题记录
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
// 将答题记录的扩展JSON转换为JSONObject列表
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
List<String> subjectIds = subjects.stream().map(QsvSubject::getId).toList();
// 查询题目选项列表
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
// 将选项列表转换为Map
Map<String, QsvOption> optionMap = options.stream().collect(Collectors.toMap(QsvOption::getId, v -> v));
// 构建Excel导出实体
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
// 构建答题记录列表
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
// 构建表格列信息
List<NutMap> tableColumns = excelExportEntities.stream()
.map(v -> NutMap.NEW().addv("prop", v.getKey()).addv("label", v.getName()))
.toList();
return NutMap.NEW()
.addv("tableColumns", tableColumns)
.addv("tableData", list);
} catch (Exception e) {
throw new RuntimeException("获取用户答题记录失败", e);
}
}
private List<NutMap> querySubjects(String activityId) {
Sql subjectSql = Sqls.create("""
SELECT
id,
title,
type,
sortNum
FROM
qsv_subject
WHERE
activityId = @activityId
ORDER BY
sortNum ASC
""");
subjectSql.setParam("activityId", activityId);
return listMap(subjectSql);
}
private List<ExcelExportEntity> buildExcelExportEntities(List<QsvSubject> subjects) {
List<ExcelExportEntity> excelExportEntities = new ArrayList<>();
excelExportEntities.add(new ExcelExportEntity("工号", "loginName", 20));
excelExportEntities.add(new ExcelExportEntity("姓名", "userName", 20));
excelExportEntities.add(new ExcelExportEntity("单位", "unitName", 20));
excelExportEntities.add(new ExcelExportEntity("分工会", "unionName", 20));
excelExportEntities.add(new ExcelExportEntity("联系方式", "mobile", 20));
for (QsvSubject subject : subjects) {
excelExportEntities.add(new ExcelExportEntity(subject.getTitle(), subject.getId(), 20));
}
return excelExportEntities;
}
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, List<QsvOption> options) {
return answerRecords.stream().map(record -> {
NutMap map = NutMap.NEW()
.addv("id", record.getId())
.addv("loginName", record.getLoginName())
.addv("userName", record.getUserName())
.addv("unitName", record.getUnitName())
.addv("unionName", record.getUnionName());
JSONObject extJson = record.getExtJson();
extJson.forEach((k, v) -> {
JSONObject jsonVal = (JSONObject) v;
String type = subjectMap.get(k).getType();
if (type.equals("text")) {
map.addv(k, jsonVal.getStr("text"));
} else if (type.equals("radio") || type.equals("checkbox")) {
JSONArray optionIds = jsonVal.getJSONArray("optionIds");
String selectOptionTexts = options.stream().filter(option -> optionIds.contains(option.getId()))
.map(QsvOption::getText).collect(Collectors.joining(";"));
map.addv(k, selectOptionTexts);
}
});
return map;
}).collect(Collectors.toList());
}
}
@@ -0,0 +1,208 @@
package io.v.nutz.zhgh.qsv.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.json.JSONObject;
import io.v.nutz.base.service.impl.BaseServiceImpl;
import io.v.nutz.base.utils.Vi;
import io.v.nutz.sys.models.User;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.qsv.models.QsvActivity;
import io.v.nutz.zhgh.qsv.models.QsvSubject;
import io.v.nutz.zhgh.qsv.models.QsvUserAnswerRecord;
import io.v.nutz.zhgh.qsv.param.QsvAnswerParam;
import io.v.nutz.zhgh.qsv.service.QsvUserAnswerRecordService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.*;
@IocBean(args = {"refer:dao"})
public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> implements QsvUserAnswerRecordService {
public QsvUserAnswerRecordServiceImpl(Dao dao) {
super(dao);
}
@Override
public QsvUserAnswerRecord insertRecord(String activityId, List<String> subjectIds) {
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
String category = activity.getCategory();
String mode = activity.getMode();
String displayMode = activity.getDisplayMode();
Boolean shuffleSubject = activity.getShuffleSubject();
//打乱题目顺序
if (category.equals("QUIZ") && shuffleSubject) {
Collections.shuffle(subjectIds);
}
QsvUserAnswerRecord record = new QsvUserAnswerRecord();
record.setActivityId(activityId);
record.setUserId(ShiroUtil.getUserId());
record.setSubjectIds(subjectIds);
JSONObject extJson = new JSONObject();
for (String subjectId : subjectIds) {
extJson.set(subjectId, Dict.create()
.set("optionIds", new ArrayList<>())
.set("text", null)
.set("isCorrect", null)
);
}
record.setExtJson(extJson);
User user = dao().fetch(User.class, Cnd.where("id", "=", ShiroUtil.getUserId()));
record.setLoginName(user.getLoginname());
record.setUserName(user.getUsername());
record.setUnitId(user.getUnitid());
record.setUnitName(user.getUnitname());
record.setUnionId(user.getUnionid());
record.setUnionName(user.getUnionname());
//问卷模式
if (category.equals("QUIZ")) {
if (mode.equals("REGULAR")) {
//常规模式 抽取随机题目
int count = dao().count(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId()));
record.setRandomNumber(count + 1);
} else if (mode.equals("SCHEDULED")) {
//定时定题模式
int count = dao().count(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", ShiroUtil.getUserId())
.and("attemptDate", "=", DateUtil.today()));
record.setRandomNumber(count + 1);
record.setAttemptDate(new Date());
}
}
dao().insert(record);
return record;
}
@Override
public float calcScore(QsvAnswerParam qsvAnswerParam) {
List<String> subjectIds = qsvAnswerParam.getSubjects().stream().map(QsvAnswerParam.Subject::getId).toList();
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
float score = 0;
for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) {
List<String> userSelectOptionIds = subject.getUserSelectOptionIds();
List<String> correctAnswer = subjects.stream().filter(s -> s.getId().equals(subject.getId())).findFirst().get().getCorrectAnswer();
String[] array1 = userSelectOptionIds.toArray(new String[0]);
String[] array2 = correctAnswer.toArray(new String[0]);
Arrays.sort(array1);
Arrays.sort(array2);
boolean isCorrect = Arrays.equals(array1, array2);
if (isCorrect) {
score += subjects.stream().filter(s -> s.getId().equals(subject.getId())).findFirst().get().getScore();
}
}
return score;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void calcByScoreMode(String activityId, String userId) {
// 获取活动信息
QsvActivity activity = dao().fetch(QsvActivity.class, activityId);
if (activity == null) {
throw new RuntimeException("活动找不到" + activityId);
}
String mode = activity.getMode();
String scoreMode = activity.getScoreMode();
if (mode.equals("SCHEDULED")) {
processScoreRecords(activityId, ShiroUtil.getUserId(), true, scoreMode.equals("HIGH"));
} else if (mode.equals("REGULAR")) {
processScoreRecords(activityId, ShiroUtil.getUserId(), false, scoreMode.equals("HIGH"));
}
}
// if (mode.equals("SCHEDULED")) {
// //定时定题模式
// if (scoreStatisticsMode.equals("HIGHEST")) {
// //也只能取当天最高的分数
// dao().update(QsvUserAnswerRecord.class, Chain.make("isHighestScore", 0), Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).and(QsvUserAnswerRecord::getAttemptDate, "=", DateUtil.today()));
// //找出最大的那条
// QsvUserAnswerRecord maxScoreRecord = dao().fetch(QsvUserAnswerRecord.class, Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).and(QsvUserAnswerRecord::getAttemptDate, "=", DateUtil.today()).desc(QsvUserAnswerRecord::getTotalScore));
// if (maxScoreRecord != null) {
// maxScoreRecord.setIsHighestScore(true);
// dao().update(maxScoreRecord);
// }
// } else if (scoreStatisticsMode.equals("LAST")) {
// //也只能取当天最新的记录
// dao().update(QsvUserAnswerRecord.class, Chain.make("isLatestScore", 0), Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).and(QsvUserAnswerRecord::getAttemptDate, "=", DateUtil.today()));
// //找出最大的那条
// QsvUserAnswerRecord latestScoreRecord = dao().fetch(QsvUserAnswerRecord.class, Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).and(QsvUserAnswerRecord::getAttemptDate, "=", DateUtil.today()).desc(QsvUserAnswerRecord::getUpdatedAt));
// if (latestScoreRecord != null) {
// latestScoreRecord.setIsLatestScore(true);
// dao().update(latestScoreRecord);
// }
// }
// } else if (mode.equals("REGULAR")) {
// if (scoreStatisticsMode.equals("HIGHEST")) {
// dao().update(QsvUserAnswerRecord.class, Chain.make("isHighestScore", 0), Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId));
// QsvUserAnswerRecord maxScoreRecord = dao().fetch(QsvUserAnswerRecord.class, Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).desc(QsvUserAnswerRecord::getTotalScore));
// if (maxScoreRecord != null) {
// maxScoreRecord.setIsHighestScore(true);
// dao().update(maxScoreRecord);
// }
// } else if (scoreStatisticsMode.equals("LAST")) {
// dao().update(QsvUserAnswerRecord.class, Chain.make("isLatestScore", 0), Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId));
// QsvUserAnswerRecord latestScoreRecord = dao().fetch(QsvUserAnswerRecord.class, Cnd.where(QsvUserAnswerRecord::getActivityId, "=", activityId).and(QsvUserAnswerRecord::getUserId, "=", userId).desc(QsvUserAnswerRecord::getUpdatedAt));
// if (latestScoreRecord != null) {
// latestScoreRecord.setIsLatestScore(true);
// dao().update(latestScoreRecord);
// }
// }
// }
/**
* @param activityId 活动id
* @param userId 用户id
* @param isScheduled 是否是定时定题模式
* @param isHighest 是否是最高分模式
*/
private void processScoreRecords(String activityId, String userId, boolean isScheduled, boolean isHighest) {
String flagField = isHighest ? "isHighestScore" : "isLatestScore";
String orderByField = isHighest ? "totalScore" : "updatedAt";
// 更新所有记录的标志位为0
Cnd cnd = Cnd.where("activityId", "=", activityId)
.and("userId", "=", userId);
if (isScheduled) {
cnd.and("attemptDate", "=", DateUtil.today());
}
dao().update(QsvUserAnswerRecord.class, Chain.make(flagField, 0), cnd);
// 找出符合条件的最大记录
Cnd cnd2 = Cnd.where("activityId", "=", activityId).and("userId", "=", userId);
if (isScheduled) {
cnd2.and("attemptDate", "=", DateUtil.today());
}
cnd2.desc(orderByField);
QsvUserAnswerRecord record = dao().fetch(QsvUserAnswerRecord.class, cnd2);
if (record != null) {
record.setIsHighestScore(isHighest);
record.setIsLatestScore(!isHighest);
dao().update(record);
}
}
}
@@ -1,17 +1,15 @@
package io.v.nutz.zhgh.question.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.wizzer.framework.base.Result;
import cn.wizzer.framework.page.Pagination;
import io.v.nutz.zhgh.activity.models.ActivityUserScope;
import io.v.nutz.base.query.PageForm;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.activity.models.ActivityUserScope;
import io.v.nutz.zhgh.question.model.*;
import io.v.nutz.zhgh.question.service.QuestionService;
import io.v.nutz.web.commons.utils.ShiroUtil;
import org.nutz.dao.Chain;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -26,9 +24,10 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
@@ -50,6 +49,7 @@ public class QuestionH5Controller {
@At("/list")
@Ok("beetl:/mobile/question/list.html")
@RequiresAuthentication
public void list() {
}
@@ -74,6 +74,7 @@ public class QuestionH5Controller {
@At("/answer/index")
@Ok("beetl:/mobile/question/answer.html")
@RequiresAuthentication
public void answerIndex() {
}
@@ -335,8 +336,25 @@ public class QuestionH5Controller {
* @return {@link Object}
*/
@At("/answerInfo/?")
public Object answerInfo(String id) {
List<Question_reply> replies = questionService.dao().query(Question_reply.class, Cnd.where("qid", "=", id).and("userId", "=", ShiroUtil.getPrincipalProperty("id")));
public Object answerInfo(String id,
@Param(required = false, value = "currentDate") String currentDate,
@Param(required = false, value = "randomCount") Integer randomCount) {
Cnd cnd = Cnd.where("qid", "=", id).and("userId", "=", ShiroUtil.getPrincipalProperty("id"));
Question question = dao.fetch(Question.class, id);
if (question.getCtMode() == 1) {
cnd.and("DATE(rtime)", "=", currentDate);
} else if (question.getCtMode() == 2) {
if (questionService.isSameDay(question.getStartTime(), question.getEndTime())) {
} else {
cnd.and("DATE(rtime)", "=", currentDate);
}
} else if (question.getCtMode() == 3) {
}
List<Question_reply> replies = questionService.dao().query(Question_reply.class, cnd);
return Result.success(replies);
}
@@ -428,6 +446,11 @@ public class QuestionH5Controller {
@Param(required = false, value = "randomCount") Integer randomCount) {
Question question = dao.fetch(Question.class, questionId);
if (question.getCanRepeatedAnswer()) {
//0次则不限制
if (question.getCanRepeatedAnswerNum() == 0) {
return Result.success(true);
}
if (question.getCtMode() == 3) {
int count = dao.count(QuestionUserScore.class, Cnd.where("questionId", "=", questionId).and("userId", "=", ShiroUtil.getUserId()));
return Result.success(count < question.getCanRepeatedAnswerNum());
@@ -1,7 +1,8 @@
package io.v.nutz.zhgh.question.service;
import io.v.nutz.zhgh.question.model.Question;
import io.v.nutz.base.service.BaseService;
import io.v.nutz.base.service.ViService;
import io.v.nutz.zhgh.question.model.Question;
import io.v.nutz.zhgh.question.model.Question_reply;
import org.nutz.lang.util.NutMap;
@@ -2,13 +2,13 @@ package io.v.nutz.zhgh.question.service.impl;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import io.v.nutz.base.service.impl.ViServiceImpl;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.question.model.*;
import io.v.nutz.zhgh.question.service.QuestionIssueService;
import io.v.nutz.zhgh.question.service.QuestionReplyService;
import io.v.nutz.zhgh.question.service.QuestionService;
import io.v.nutz.zhgh.question.service.QuestionWorService;
import io.v.nutz.base.service.impl.ViServiceImpl;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
@@ -0,0 +1,131 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="列表" fixed placeholder left-text="返回" left-arrow @click-left="historyBack"></van-nav-bar>
<van-sticky offset-top="46px">
<van-tabs v-model="pageForm.category" @change="onRefresh">
<van-tab v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :name="item.code" :title="item.label" :key="item.code"></van-tab>
</van-tabs>
</van-sticky>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh" style="min-height: calc(100vh - 90px)">
<van-list v-if="list && list.length>0" v-model="loading" :finished="finished" finished-text="" @load="onLoad">
<div v-for="row in list" :key="row.id">
<div
style="display: flex; padding: 15px; margin: 20px; box-sizing: border-box; background: #ffffff; border-radius: 10px"
@click="itemClick(row)"
>
<div class="list-item-icon" style="width: 96px; height: 80px; flex-shrink: 0">
<img src="/assets/mobile/svg/qsv/icon.svg" alt="" style="width: 100%; height: 100%" />
</div>
<div style="flex-grow: 1; display: flex; flex-direction: column; justify-content: space-around">
<div
style="
font-weight: bolder;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
"
>
{{row.title}}
</div>
<div style="font-size: 13px; color: #606266">
开始时间:{{row.startTime}}
<br />
结束时间:{{row.endTime}}
</div>
</div>
</div>
</div>
</van-list>
<van-empty v-if="list.length===0" description="没有更多了"></van-empty>
</van-pull-refresh>
</div>
<script>
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
data() {
return {
loading: false,
finished: false,
refreshing: false,
list: [],
pageForm: {
pageNumber: 1,
pageSize: 10,
category: "QUIZ"
},
categoryPaths: {
QUIZ: "/platform/h5/qsv/quiz",
SURVEY: "/platform/h5/qsv/survey",
VOTE: "/platform/h5/qsv/vote"
}
}
},
methods: {
onLoad() {
this.loading = true
const loading = createListLoading()
this.$axios
.post("/platform/h5/qsv/pageData", this.pageForm)
.then((res) => {
if (res.code === 0) {
this.list = this.list.concat(res.data.list)
this.pageForm.totalCount = res.data.totalCount
if (this.list.length >= this.pageForm.totalCount) {
this.finished = true
}
this.pageForm.pageNumber++
}
})
.finally(() => {
loading.close()
this.loading = false
this.refreshing = false
})
},
onRefresh() {
this.pageForm.pageNumber = 1
this.list = []
this.finished = false
this.onLoad()
},
async itemClick(item) {
if (item.groupId) {
const { code, data } = await this.$axios.post("/open/common/checkGroupPermission", { groupId: item.groupId })
if (code === 0) {
if (!data) {
this.$toast.fail("您没有权限参与")
return
}
}
}
const { startTime, endTime } = item
if (this.$moment(startTime).unix() > this.$moment().unix()) {
this.$toast("未开始")
return
}
const path = this.categoryPaths[item.category]
if (path) {
this.$pjaxReplace(path + "?id=" + item.id)
}
}
},
created() {
this.onLoad()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,370 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
/*margin: 10px 0;*/
margin-bottom: 10px;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.timer {
text-align: center;
font-size: 18px;
background: #fff;
padding: 10px 0;
}
.submit-button {
width: 100%;
margin-top: 20px;
}
.result {
text-align: center;
font-size: 20px;
margin-top: 20px;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<!--开启计时 未完成 活动未结束-->
<div class="timer" v-if="activity.timeLimit > 0 && !answerRecord.isFinish && !isEnd">⏰{{ remainingTime }}s</div>
<h2 class="title">{{ activity.title }}</h2>
</van-sticky>
<div v-if="!answerRecord.isFinish">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<van-checkbox-group v-model="subject.userSelectOptionIds" :ref="'subject'+subject.id">
<van-cell-group>
<van-cell
v-for="(option,index) in subject.options"
clickable
:key="option.id"
:title="option.text"
@click="cellToggle(subject,option.id)"
>
<template #icon>
<van-checkbox
:name="option.id"
:ref="'option'+option.id"
:disabled="answerRecord.isFinish"
:shape="subject.type==='checkbox' ? 'square' : 'round'"
style="margin-right: 10px"
></van-checkbox>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</div>
<div class="button-control" v-if="!answerRecord.isFinish">
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
</div>
</div>
</div>
</div>
<script>
const vue = new Vue({
el: "#app",
data() {
return {
list: [],
id: GetQueryString("id"),
subjects: [],
activity: {},
// remainingTime: 0,
isFinished: false,
answerRecord: {},
answerRecordId: null,
//历史记录
historyScores: [],
//答题用时
answerTime: 0
}
},
computed: {
isEnd() {
if (this.activity) {
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
}
return true
},
remainingTime() {
if (this.activity && this.activity.timeLimit > 0) {
return this.activity.timeLimit * 60 - this.answerTime
}
return 0
}
},
methods: {
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/quiz/subjects", { activityId: this.id }).then((res) => {
if (res.code === 0) {
this.activity = res.data.activity
this.subjects = res.data.subjects
this.answerRecordId = res.data.answerRecordId
this.checkGroupPermission()
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/quiz/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
if (this.answerRecord.isFinish || this.$moment().unix() > this.$moment(this.activity.endTime)) {
this.$pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
}
this.initAnswer()
this.checkTimer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
if (this.answerRecord.optionIds) {
this.answerRecord.optionIds[subjectIndex].forEach((optionId) => {
this.$refs["option" + optionId][0].toggle()
})
}
})
},
//答题计时器
checkTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
//开启计时
const startInterval = () => {
this.timerInterval = setInterval(() => {
this.answerTime++
if (this.activity.timeLimit > 0 && this.answerTime >= this.activity.timeLimit * 60) {
clearInterval(this.timerInterval)
const loading = this.$toast.loading({
message: "答题时间到,自动提交中",
forbidClick: true
})
setTimeout(() => {
this.autoSubmit(loading)
}, 1500)
}
}, 1000)
}
if (!this.isEnd && !this.answerRecord.isFinish) {
if (this.activity.timeLimit > 0) {
this.$dialog
.alert({
title: "提示",
message: "本次答题时间" + this.activity.timeLimit + "分钟,点击确认开始答题"
})
.then(() => {
startInterval()
})
} else {
startInterval()
}
}
// if (this.activity.timeLimit > 0 && !this.isEnd) {
// if (!this.answerRecord.isFinish) {
// this.$dialog
// .alert({
// title: "提示",
// message: "本次答题时间" + this.activity.timeLimit + "分钟,点击确认开始答题"
// })
// .then(() => {
// this.remainingTime = this.activity.timeLimit * 60
// this.timerInterval = setInterval(() => {
// if (this.remainingTime > 0) {
// this.remainingTime--
// } else {
// clearInterval(this.timerInterval)
// const loading = this.$toast.loading({
// message: "答题时间到,自动提交中",
// forbidClick: true
// })
// this.autoSubmit(loading)
// }
// }, 1000)
// })
// }
// }
},
historyScore() {
this.$axios.post("/platform/h5/qsv/quiz/historyScore", { activityId: this.activityId }).then((res) => {
if (res.code === 0) {
this.historyScores = res.data
}
})
},
//选项点击
cellToggle(subject, optionId) {
if (this.answerRecord.isFinish) {
return
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
this.$refs["option" + optionId][0].toggle()
},
//手动提交
onSubmit() {
if (this.isEnd) {
this.$toast("调查已结束")
return
}
//提示那些题没有作答
for (let i = 0; i < this.subjects.length; i++) {
let subject = this.subjects[i]
//["radio", "checkbox"].includes(subject.type) &&
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
}
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
this.autoSubmit()
},
//自动提交
autoSubmit(loading = null) {
this.$axios
.post("/platform/h5/qsv/quiz/submitAnswer", {
answer: JSON.stringify({
activityId: this.id,
answerRecordId: this.answerRecordId,
subjects: this.subjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : []
}
}),
answerTime: this.answerTime
})
})
.then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.$pjaxReplace("/platform/h5/qsv/quiz/result?answerRecordId=" + this.answerRecordId)
// this.getAnswerRecord()
}
})
.finally(() => {
if (loading) {
loading.clear()
}
})
},
async checkGroupPermission() {
const groupId = this.activity.groupId
if (groupId) {
const { code, data } = await this.$axios.post("/open/common/checkGroupPermission", { groupId })
if (code === 0) {
if (!data) {
this.$dialog
.alert({
title: "提示",
message: "您没有权限参与"
})
.then(() => {
location.back()
})
}
}
}
}
},
created() {
if (this.timerInterval) {
clearInterval(this.timerInterval)
}
if (this.id) {
this.listSubjects()
}
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,330 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
color: var(--color-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.container .score {
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.score-form {
background: #ffffff;
display: flex;
margin-bottom: 10px;
border-top: 1px solid #f1f1f1;
align-items: center;
}
.score-form-total {
flex: 1;
background: #fff;
display: flex;
padding: 10px 20px 10px 0;
flex-direction: column;
align-items: flex-end;
}
.score-text-news {
flex: 1;
background: #fff;
}
.score-font-style {
font-size: 32px;
color: #ff6a00;
word-break: keep-all;
line-height: 38px;
min-width: 47px;
text-align: center;
}
.score-underline {
background: url(//image.wjx.cn/images/newimg/score-form/score-underline@2x.png) no-repeat center;
background-size: 47px 16px;
display: inline-block;
height: 16px;
width: 47px;
margin-top: 7px;
}
.van-checkbox__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.van-checkbox__icon--disabled .van-icon {
background-color: #fff;
}
.answer-container {
padding: 16px;
}
.correct-answer {
color: green;
}
.incorrect-answer {
color: red;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
.history-list {
background: #f1f1f1;
padding: 10px;
}
.history-item {
background: #ffffff;
border-radius: 12px;
padding: 16px;
margin: 20px 10px;
box-shadow:
8px 8px 16px #d9d9d9,
-8px -8px 16px #ffffff;
transition: transform 0.2s ease-in-out;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-header span {
font-size: 14px;
font-weight: 600;
color: #333;
}
.history-header .tag {
margin-left: 8px;
color: #fff;
padding: 2px 5px;
}
.history-info {
font-size: 14px;
color: #555;
margin-bottom: 4px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<van-sticky offset-top="46px">
<h2 class="title">{{ activity.title }}</h2>
<div class="score-form">
<div class="score-form-total">
<div class="score-font-style">{{totalScore}}</div>
<i class="score-underline"></i>
</div>
</div>
</van-sticky>
<div>
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<van-checkbox-group v-model="subject.userSelectOptionIds" :ref="'subject'+subject.id">
<van-cell-group>
<van-cell v-for="(option,index) in subject.options" clickable :key="option.id" :title="option.text">
<template #title>
<span :style="{color : option.isCorrect ? 'green' : ''}">{{option.text}}</span>
</template>
<template #icon>
<van-checkbox
:name="option.id"
:ref="'option'+option.id"
:disabled="answerRecord.isFinish"
style="margin-right: 10px"
></van-checkbox>
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<div class="answer-container">
<div v-if="answerRecord.extJson?.[subject.id]?.isCorrect" class="correct-answer">
<van-icon name="passed"></van-icon>
回答正确
</div>
<div v-else-if="answerRecord.extJson?.[subject.id]" class="incorrect-answer">
<van-icon name="close"></van-icon>
回答错误
</div>
</div>
</div>
<div class="button-control">
<van-button type="primary" @click="openHistory" block>查看全部答题记录</van-button>
</div>
</div>
<van-action-sheet v-model="historyShow" title="历史记录">
<div class="history-list">
<div v-for="item in historyList" class="history-item">
<div class="history-header">
<span>{{ item.date }}</span>
<van-tag v-if="item.isHighestScore" type="danger" class="tag">最高分</van-tag>
<van-tag v-if="item.isLatestScore" type="primary" class="tag">最新得分</van-tag>
</div>
<div class="history-info">答题分数:{{ item.totalScore }}</div>
<div class="history-info">答题用时:{{ item.answerTime | formatSeconds }}</div>
</div>
</div>
</van-action-sheet>
</div>
</div>
<script>
const vue = new Vue({
el: "#app",
data() {
return {
answerRecordId: GetQueryString("answerRecordId"),
answerRecord: {},
subjects: [],
activity: {},
list: [],
historyShow: false,
historyList: []
}
},
computed: {
totalScore() {
if (!this.answerRecord || !this.answerRecord.extJson) {
return 0
}
let totalScore = 0
for (const key in this.answerRecord.extJson) {
if (this.answerRecord.extJson.hasOwnProperty(key)) {
totalScore += this.answerRecord.extJson[key].score || 0
}
}
return totalScore
}
},
filters: {
formatSeconds(seconds) {
if (seconds) {
const minutes = Math.floor(seconds / 60) // 获取总分钟数
const remainingSeconds = seconds % 60 // 获取剩余秒数
// 格式化输出,确保秒数始终为两位数
return minutes + "分钟" + String(remainingSeconds).padStart(2, "0") + "秒"
}
return null
}
},
methods: {
getAnswerResult() {
this.$axios.post("/platform/h5/qsv/quiz/answerResult", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.subjects = res.data.subjects
this.activity = res.data.activity
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/quiz/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
this.initAnswer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
const answer = this.answerRecord.extJson[subject.id]
if (["radio", "checkbox"].includes(subject.type)) {
this.$nextTick(() => {
this.$refs["subject" + subject.id][0].toggleAll(false)
})
answer?.optionIds.forEach((optionId) => {
this.$nextTick(() => {
this.$refs["option" + optionId][0].toggle()
})
})
} else if (subject.type === "text") {
subject.userFillContent = answer?.text
}
})
},
historyBack() {
this.$pjaxReplace("/platform/h5/qsv")
},
openHistory() {
this.historyShow = true
this.$axios.post("/platform/h5/qsv/quiz/historyScore", { activityId: this.activity.id }).then((res) => {
if (res.code === 0) {
this.historyList = res.data
}
})
}
},
created() {
this.getAnswerResult()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,292 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.container {
padding: 0;
}
.container .title {
text-align: center;
padding: 20px;
background: #ffffff;
margin: 0 0 10px 0;
}
.container .card {
padding: 10px;
background: #ffffff;
}
.subject {
background: #fff;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.subject p {
font-size: 16px;
margin: 20px 0 10px 0;
font-weight: bold;
}
.subject .tag {
position: absolute;
top: 0;
}
.button-control {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
display: flex;
column-gap: 20px;
background: #fff;
}
.van-checkbox__icon--checked .van-icon {
color: #fff !important;
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
.van-checkbox__icon--disabled .van-icon {
background-color: #fff;
}
.ui-input-box {
border: 1px solid #e3e3e3;
margin: 5px 0;
background-color: #fff;
padding: 0;
width: 100%;
display: flex;
}
.ui-input-box input {
background-color: #fff;
border: none !important;
padding: 10px;
font-size: 14px;
line-height: 18px;
display: inline-block;
margin: 0;
-webkit-appearance: none;
resize: none;
width: 100%;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="问卷调查" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="container" :style="{'padding-bottom': answerRecord.isFinish ? 0 : '84px'}">
<h2 class="title">{{ activity.title }}</h2>
<div v-if="!isFinished">
<div v-for="(subject, index) in subjects" :key="index" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>
<van-tag v-if="subject.score" type="primary" size="large">{{subject.score}}分</van-tag>
</div>
<!--单选、多选-->
<van-checkbox-group
v-model="subject.userSelectOptionIds"
:ref="'subject'+subject.id"
v-if="['radio','checkbox'].includes(subject.type)"
>
<van-cell-group>
<van-cell
v-for="(option,index) in subject.options"
clickable
:key="option.id"
:title="option.text"
@click="cellToggle(subject,option.id)"
>
<template #icon>
<van-checkbox :name="option.id" :ref="'option'+option.id" style="margin-right: 10px"></van-checkbox>
</template>
<img
slot="right-icon"
v-if="option.imgUrl"
:src="option.imgUrl"
alt=""
style="width: 40px; height: 40px"
@click.stop="previewOptionImg(option.imgUrl)"
/>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<!--填空题-->
<div class="ui-input-box" v-if="subject.type==='text'">
<input type="text" v-model="subject.userFillContent" :readonly="answerRecord.isFinish" />
</div>
</div>
<div class="button-control" v-if="!answerRecord.isFinish">
<van-button type="primary" @click="onSubmit" block :disabled="isEnd">{{isEnd ? '已结束' : '提交'}}</van-button>
</div>
</div>
</div>
</div>
<script>
const vue = new Vue({
el: "#app",
data() {
return {
list: [],
id: null,
subjects: [],
activity: {},
remainingTime: 0,
isFinished: false,
answerRecord: {},
answerRecordId: null
}
},
computed: {
isEnd() {
if (this.activity) {
return this.$moment().unix() > this.$moment(this.activity.endTime).unix()
}
return true
}
},
methods: {
//获取活动、题目
listSubjects() {
this.$axios.post("/platform/h5/qsv/survey/subjects", { activityId: this.id }).then((res) => {
if (res.code === 0) {
this.activity = res.data.activity
this.subjects = res.data.subjects
this.answerRecordId = res.data.answerRecordId
this.getAnswerRecord()
}
})
},
//获取回答记录
getAnswerRecord() {
this.$axios.post("/platform/h5/qsv/survey/answerRecord", { answerRecordId: this.answerRecordId }).then((res) => {
if (res.code === 0) {
this.answerRecord = res.data
if (this.answerRecord.isFinish) {
this.$toast.success("您已完成该调查")
}
this.initAnswer()
}
})
},
//答案回显
initAnswer() {
this.subjects.forEach((subject, subjectIndex) => {
const answer = this.answerRecord.extJson[subject.id]
if (["radio", "checkbox"].includes(subject.type)) {
this.$refs["subject" + subject.id][0].toggleAll(false)
answer?.optionIds.forEach((optionId) => {
this.$nextTick(() => {
this.$refs["option" + optionId][0].toggle()
})
})
} else if (subject.type === "text") {
subject.userFillContent = answer?.text
}
})
},
//选项点击
cellToggle(subject, optionId) {
if (this.answerRecord.isFinish) {
return
}
if (subject.type === "radio") {
this.$refs["subject" + subject.id][0].toggleAll(false)
}
this.$refs["option" + optionId][0].toggle()
},
//预览图片
previewOptionImg(img) {
vant.ImagePreview([img])
},
//手动提交
onSubmit() {
const endTime = this.activity.endTime
if (this.isEnd) {
this.$toast("调查已结束")
return
}
//提示那些题没有作答
for (let i = 0; i < this.subjects.length; i++) {
const subject = this.subjects[i]
if (["radio", "checkbox"].includes(subject.type)) {
if (!subject.userSelectOptionIds || subject.userSelectOptionIds.length === 0) {
this.$toast.fail("第" + (i + 1) + "题未做答")
return
}
} else if ("text" === subject.type) {
if (!subject.userFillContent) {
this.$toast.fail("第" + (i + 1) + "题未作答")
return
}
}
}
this.autoSubmit()
},
//自动提交
autoSubmit(loading = null) {
this.$axios
.post("/platform/h5/qsv/survey/submitAnswer", {
answer: JSON.stringify({
activityId: this.id,
answerRecordId: this.answerRecordId,
subjects: this.subjects.map((subject) => {
return {
id: subject.id,
userSelectOptionIds: ["checkbox", "radio"].includes(subject.type) ? subject.userSelectOptionIds : [],
userFillContent: subject.type === "text" ? subject.userFillContent : null
}
})
})
})
.then((res) => {
if (res.code === 0) {
this.$toast.success(res.msg)
this.getAnswerRecord()
}
})
.finally(() => {
if (loading) {
loading.clear()
}
})
}
},
created() {
const id = GetQueryString("id")
if (id) {
this.id = id
this.listSubjects()
}
}
})
</script>
<!--#
}
#-->
@@ -223,11 +223,6 @@ layout("/mobile/platform.html"){
<van-col span="16" v-if="wor.wsm">
{{ wor.wsm }}
</van-col>
<van span="24" v-if="wor.files&&wor.files.length>0">
<div v-for="file in wor.files">
<div @click="fileClick(file)"> {{file.filename}}</div>
</div>
</van>
</van-row>
</div>
</div>
@@ -309,10 +304,6 @@ layout("/mobile/platform.html"){
}
},
methods: {
fileClick(file){
let pdfStreamUrl = encodeURIComponent(FILE_STREAM_PREVIEW_ADDRESS + '?id=' + file.id) //将路径转码
window.open('/assets/platform/plugins/pdfJs/web/viewer.html?file=' + pdfStreamUrl,file. filename)
},
async getQuestionData() {
const resp = await $.get('/mobile/question/h5/questionInfo/' + this.id)
this.questionData = resp.data
@@ -0,0 +1,252 @@
const basicForm = {
template: /*language=HTML*/ `
<el-dialog :visible.sync="dialogVisible" title="基础设置">
<div style="overflow-y: auto">
<el-form :model="formData" ref="formRef" :rules="formRules" label-width="150px">
<el-form-item label="类型" prop="category">
<el-radio-group v-model="formData.category" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_CATEGORY" :label="item.code"
:key="item.code"
border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" maxlength="50"
show-word-limit></el-input>
</el-form-item>
<el-form-item label="说明" prop="description">
<text-editor v-model="formData.description" :height="150"></text-editor>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker v-model="formData.startTime" type="datetime" placeholder="选择日期时间"
style="width: 100%"
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结束时间" prop="endTime">
<el-date-picker v-model="formData.endTime" type="datetime" placeholder="选择日期时间"
style="width: 100%"
value-format="yyyy-MM-dd HH:mm:ss"></el-date-picker>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="参加人员范围" prop="groupId">
<div style="display: flex;column-gap: 10px">
<el-select clearable filterable
placeholder="参加人员范围"
prop="groupId"
style="width: 99%;"
v-model="formData.groupId">
<el-option :key="item.groupId"
:label="item.groupName"
:value="item.groupId"
v-for="item in activityGroupOptions"></el-option>
</el-select>
<el-button
@click="$refs.drawerUserScope.userScopeDialog = true"
type="primary">设置
</el-button>
</div>
</el-form-item>
<template v-if="formData.category==='QUIZ'">
<el-form-item label="出题模式" prop="mode">
<span slot="label">
出题模式
<el-popover placement="bottom" trigger="hover">
<div>
<p>定时定题:例如一周答题,每天显示不同的题目,需要在题目设置显示日期。</p>
<p>常规模式:无</p>
</div>
<i class="el-icon-question" slot="reference"></i>
</el-popover>
</span>
<el-radio-group v-model="formData.mode" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_MODE" :label="item.code"
:key="item.code"
border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="题目顺序打乱" prop="shuffleSubject">
<span slot="label">
题目顺序打乱
<el-popover placement="bottom" trigger="hover">
<div>
题目都是相同的,只是顺序不一致。
</div>
<i class="el-icon-question" slot="reference"></i>
</el-popover>
</span>
<el-switch v-model="formData.shuffleSubject"></el-switch>
</el-form-item>
<el-form-item label="题目显示模式" prop="displayMode" v-if="formData.category==='QUIZ' && formData.mode==='REGULAR'">
<span slot="label">
题目显示模式
<el-popover placement="bottom" trigger="hover">
<div>
注:定时定题模式无需配置。
<p>
常规模式下配置例如:题库50题,只需抽取其中20题答题则需配置为随机抽取模式。
</p>
</div>
<i class="el-icon-question" slot="reference"></i>
</el-popover>
</span>
<el-radio-group v-model="formData.displayMode" size="small">
<el-radio label="ALL" border>全部</el-radio>
<el-radio label="RANDOM" border>随机抽取</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="单次随机抽取题数" prop="randomCount"
v-if="formData.category==='QUIZ' && formData.mode==='REGULAR' && formData.displayMode==='RANDOM'">
<el-input-number v-model="formData.randomCount" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item label="总随机抽取次数" prop="totalRandom"
v-if="formData.category==='QUIZ' && formData.mode==='REGULAR' && formData.displayMode==='RANDOM'">
<el-input-number v-model="formData.totalRandom" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item label="是否可重复答题" prop="repeatable" v-if="formData.mode==='SCHEDULED' || (formData.mode==='REGULAR' && formData.displayMode==='ALL')">
<el-switch v-model="formData.repeatable"></el-switch>
</el-form-item>
<!-- <el-form-item label="重复答题模式" prop="repeatMode" v-if="formData.repeatable">-->
<!-- <span slot="label">-->
<!-- 重复答题模式(作废)-->
<!-- <el-popover placement="bottom" trigger="hover">-->
<!-- <div>-->
<!-- 定时定题模式下每天即用户每天可答题多次-->
<!-- </div>-->
<!-- <i class="el-icon-question" slot="reference"></i>-->
<!-- </el-popover>-->
<!-- </span>-->
<!-- <el-radio-group v-model="formData.repeatMode" size="small">-->
<!-- <el-radio v-for="item in dict.type.ACTIVITY_QSV_REPEAT_MODE" :label="item.code"-->
<!-- :key="item.code" border>-->
<!-- {{item.label}}-->
<!-- </el-radio>-->
<!-- </el-radio-group>-->
<!-- </el-form-item>-->
<el-form-item label="最大答题次数" prop="maxAttempts" v-if="formData.repeatable && (formData.mode==='SCHEDULED' || (formData.mode==='REGULAR' && formData.displayMode==='ALL'))">
<el-input-number v-model="formData.maxAttempts" :min="1" :max="100"></el-input-number>
</el-form-item>
<el-form-item label="时间限制(min)" prop="timeLimit">
<span slot="label">
时间限制(min)
<el-popover placement="bottom" trigger="hover">
<div>
无需限制请设置为0
</div>
<i class="el-icon-question" slot="reference"></i>
</el-popover>
</span>
<el-input-number v-model="formData.timeLimit" :min="0" :max="100"
placeholder="无需限制请设置为0"></el-input-number>
</el-form-item>
<el-form-item label="得分统计模式" prop="scoreMode" v-if="formData.category==='QUIZ'">
<el-radio-group v-model="formData.scoreMode" size="small">
<el-radio v-for="item in dict.type.ACTIVITY_QSV_SCORE_MODE" :label="item.code"
:key="item.code" border>
{{item.label}}
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="封面" prop="cover">-->
<!-- <file-upload-->
<!-- :upload_number="1"-->
<!-- :value.sync="formData.cover"-->
<!-- accept=".jpg,.jpeg,.png"-->
<!-- complete_result-->
<!-- upload_mode="image"-->
<!-- upload_result_category="interval"-->
<!-- ></file-upload>-->
<!-- </el-form-item>-->
</template>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="onSubmit">确 定</el-button>
</div>
<drawer-user-scope
@group_change="getActivityGroup"
ref="drawerUserScope"
:group_id.sync="formData.groupId"
></drawer-user-scope>
</el-dialog>
`,
dicts: ["ACTIVITY_QSV_CATEGORY", "ACTIVITY_QSV_MODE", "ACTIVITY_QSV_REPEAT_MODE", "ACTIVITY_QSV_SCORE_MODE"],
components: {
"drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue")
},
data() {
return {
dialogVisible: false,
formData: {},
formRules: {
category: [{ required: true, message: "请选择类型", trigger: "change" }],
title: [{ required: true, message: "请输入标题", trigger: "change" }],
startTime: [{ required: true, message: "请选择开始时间", trigger: "change" }],
endTime: [{ required: true, message: "请选择结束时间", trigger: "change" }],
mode: [{ required: true, message: "请选择出题模式", trigger: "change" }],
scoreMode: [{ required: true, message: "请选择得分统计模式", trigger: "change" }]
},
activityGroupOptions: []
}
},
methods: {
onOpen(id) {
this.dialogVisible = true
this.getActivityGroup()
if (id) {
this.$axios.post("/platform/qsv/activity/findOne", { id }).then((res) => {
if (res.code === 0) {
this.formData = res.data
}
})
} else {
this.formData = {}
}
},
onSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
this.$axios.post("/platform/qsv/activity/save", this.formData).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.dialogVisible = false
this.$emit("refresh", null)
}
})
}
})
},
getActivityGroup() {
this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => {
if (res.code === 0) {
this.activityGroupOptions = res.data
}
})
}
}
}
@@ -0,0 +1,100 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app">
<guava ref="guava">
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker placeholder="年度" type="year" v-model="pageForm.year" value-format="yyyy"></el-date-picker>
</search-item>
<search-item label="标题">
<el-input placeholder="标题" v-model="pageForm.title" clearable></el-input>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button @click="$refs.basicFormRef.onOpen()" size="small" type="primary" class="mr5">新增</el-button>
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable width="200">
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="300px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<template #edit>
<subject-form ref="subjectFormRef" @refresh="pageData"></subject-form>
</template>
</guava>
<basic-form ref="basicFormRef" @refresh="pageData"></basic-form>
<!-- <subject-form ref="subjectFormRef" @refresh="pageData"></subject-form>-->
</div>
<script>
<!--#include('basicForm.js'){}#-->
<!--#include('subjectForm.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
mixins: [initTableMixins],
components: {
"basic-form": basicForm,
"subject-form": subjectForm
},
data() {
return {}
},
methods: {
openEdit(row) {
this.$refs.basicFormRef.onOpen(row.id)
},
openSubject(id) {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$refs.subjectFormRef.onOpen(id)
})
// this.$refs.subjectFormRef.onOpen(id)
},
onDelete(id) {
this.$confirm("您确认删除吗, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/qsv/activity/delete", { id }).then((res) => {
if (res.code === 0) {
this.$message.success("删除成功")
this.pageData()
}
})
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,83 @@
const optionImg = {
template: /*language=HTML*/ `
<el-dialog :visible="visible" title="设置图片" append-to-body width="700px">
<div style="background: #f7f8f9;text-align: center;border: solid 1px #d7d8d9;border-radius: 4px;">
<el-upload
action="/platform/sys/file/uploadDynamicReturnUrl"
:show-file-list="false"
:on-success="handleSuccess"
:before-upload="beforeUpload">
<img v-if="img" :src="img" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
<div style="padding: 10px 0">
请上传图片
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false">取 消</el-button>
<el-button type="primary" @click="onConfirm">确 定</el-button>
</div>
</el-dialog>
`,
data() {
return {
visible: false,
img: null,
ext: null
}
},
methods: {
onOpen(img = null, ext) {
this.img = img
this.ext = ext
this.visible = true
},
handleSuccess(response, file, fileList) {
console.log(response)
console.log(file)
console.log(fileList)
if (response.code === 0) {
this.img = response.data
// this.img = "/platform/sys/file/download?id=t38dmq4beuhrupqb54prl52t4u"
this.$message.success("图片上传成功")
}
},
beforeUpload(file) {},
onConfirm() {
this.$emit("confirm", {
img: this.img,
ext: this.ext
})
this.visible = false
}
},
style: /*language=CSS*/ `
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
`
}
@@ -0,0 +1,494 @@
<!--#include('optionImg.js'){}#-->
const subjectForm = {
template: /*language=HTML*/ `
<div class="subject-form-dialog">
<el-form ref="form" :model="formData" :rules="formRules" label-width="120px">
<div style="min-height:50vh;overflow-y: auto">
<draggable v-model="subjects" handle=".drag-handler">
<transition-group>
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
<div class="subject-header">
<div style="display: flex; align-items: center;">
<i class="el-icon-rank drag-handler"></i>
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
<el-input
v-model="subject.title"
placeholder="请输入题目"
style="flex: 1">
</el-input>
</div>
<div class="subject-meta">
<div>
题目类型:
<el-select
v-model="subject.type"
@change="subjectTypeChange(subject, subjectIndex)"
placeholder="请选择题目类型"
style="width: 200px;">
<el-option label="单选题" value="radio">
<i class="el-icon-circle-check subject-type-icon"></i>单选题
</el-option>
<el-option label="多选题" value="checkbox">
<i class="el-icon-check subject-type-icon"></i>多选题
</el-option>
<el-option label="填空题" value="text" v-if="activity.category!=='QUIZ'">
<i class="el-icon-edit subject-type-icon"></i>填空题
</el-option>
<!-- <el-option label="判断题" value="judge">-->
<!-- <i class="el-icon-right subject-type-icon"></i>判断题-->
<!-- </el-option>-->
</el-select>
</div>
<!-- <el-input-->
<!-- v-model="subject.hint"-->
<!-- placeholder="题目提示信息"-->
<!-- style="width: 200px;">-->
<!-- <template slot="prepend">提示</template>-->
<!-- </el-input>-->
<div v-if="activity.category==='QUIZ'">
题目分数:
<el-input-number
v-model="subject.score"
:min="0"
:max="100"
placeholder="分数">
<template slot="prepend">分数</template>
</el-input-number>
</div>
<div v-if="activity.category==='QUIZ' && activity.mode==='SCHEDULED'">
显示日期:
<el-date-picker
v-model="subject.displayDate"
type="date"
placeholder="显示日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd">
</el-date-picker>
</div>
</div>
</div>
<template>
<draggable v-model="subject.options" handle=".option-drag-handle"
v-if="subject.type !== 'text'">
<transition-group>
<div v-for="(option, optionIndex) in subject.options"
:key="option.id"
class="option-item">
<i class="el-icon-rank option-drag-handle"></i>
<div class="option-content">
<el-input
v-model="option.text"
:placeholder="'选项'+optionIndex + 1">
<template slot="prepend">选项{{optionIndex + 1}}</template>
</el-input>
<!-- <el-input-->
<!-- v-model="option.hint"-->
<!-- placeholder="选项提示"-->
<!-- style="margin-top: 5px;">-->
<!-- </el-input>-->
<div v-if="option.imageUrl" style="margin-top: 10px;">
<img :src="option.imageUrl" class="image-preview" alt="">
</div>
</div>
<div class="option-tools">
<template v-if="activity.category!=='QUIZ'">
<div class="option-img-box" v-if="option.imgUrl">
<i class="el-icon-remove"
@click="removeOptionImg(subjectIndex,optionIndex)"></i>
<img :src="option.imgUrl" alt=""
@click="openOptionImg(subjectIndex,optionIndex,option)">
</div>
<i v-if="!option.imgUrl" class="el-icon-picture-outline"
style="font-size: 40px;cursor: pointer;" title="上传图片"
@click="openOptionImg(subjectIndex,optionIndex,option)"></i>
</template>
<el-switch
v-if="activity.category==='QUIZ'"
v-model="option.isCorrect"
active-text="正确答案">
</el-switch>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="removeOption(subject, optionIndex)">
</el-button>
</div>
</div>
</transition-group>
</draggable>
<div class="subject-toolbar">
<div>
<el-button
v-if="subject.type !== 'text'"
type="primary"
icon="el-icon-plus"
size="small"
@click="addOption(subject)">
添加选项
</el-button>
</div>
<div>
<el-button
type="danger"
icon="el-icon-delete"
size="small"
@click="removeSubject(subjectIndex)">
删除题目
</el-button>
</div>
<!-- <div>-->
<!-- <el-switch-->
<!-- v-model="subject.required"-->
<!-- active-text="必答题">-->
<!-- </el-switch>-->
<!-- <el-switch-->
<!-- v-model="subject.showDate"-->
<!-- class="date-visible"-->
<!-- active-text="显示日期">-->
<!-- </el-switch>-->
<!-- </div>-->
</div>
</template>
<div class="meta-info">
<!-- 创建时间:{{ formatDate(subject.createTime) }}-->
<!-- <el-button-->
<!-- type="danger"-->
<!-- icon="el-icon-delete"-->
<!-- size="mini"-->
<!-- style="float: right;"-->
<!-- @click="removeSubject(index)">-->
<!-- 删除题目-->
<!-- </el-button>-->
</div>
</div>
</transition-group>
</draggable>
<el-empty description="描述文字" v-if="subjects.length===0"></el-empty>
</div>
</el-form>
<div style="border-top: 1px solid var(--border-color-lighter);padding: 12px;text-align: right;">
<!-- <el-button @click="visible = false">取消</el-button>-->
<el-button type="primary" icon="el-icon-plus" @click="addSubject">
添加题目
</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveQuestionnaire">
保存题目
</el-button>
</div>
<option-img ref="optionImgRef" @confirm="onOptionImgConfirm"></option-img>
</div>
`,
components: {
'option-img': optionImg,
},
data() {
return {
id: null,
visible: false,
formData: {},
subjects: [],
previewDialogVisible: false,
activity: {},
formRules: {}
}
},
methods: {
onOpen(id) {
this.visible = true
if (id) {
this.id = id
this.$axios.post("/platform/qsv/activity/findOne", {id}).then((res) => {
if (res.code === 0) {
this.activity = res.data
}
})
this.$axios.post("/platform/qsv/activity/listSubjects", {activityId: id}).then((res) => {
if (res.code === 0) {
this.subjects = res.data
}
})
}
},
//生成随机id
generateId() {
return Date.now() + Math.random().toString(36).substr(2, 9)
},
//添加题目
addSubject() {
this.subjects.push({
id: this.generateId(),
title: "",
hint: "",
type: "radio",
score: 0,
displayDate: "",
options: [
{
id: this.generateId(),
text: "选项1",
hint: "",
imgUrl: null,
isCorrect: false
},
{
id: this.generateId(),
text: "选项2",
hint: "",
imgUrl: null,
isCorrect: false
}
],
})
},
//题目类型切换
subjectTypeChange(subject, subjectIndex){
if(subject.type==='text'){
subject.options = []
}
},
//删除题目
removeSubject(index) {
this.$confirm("确认删除该题目?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.subjects.splice(index, 1)
this.$message.success("删除成功")
})
.catch(() => {
})
},
//添加选项
addOption(subject) {
subject.options.push({
id: this.generateId(),
text: "选项" + subject.options.length + 1,
hint: "",
imgUrl: null,
isCorrect: false
})
},
//删除选项
removeOption(subject, optionIndex) {
subject.options.splice(optionIndex, 1)
},
//打开选项上传图片
openOptionImg(subjectIndex, optionIndex, option) {
this.$refs.optionImgRef.onOpen(option.imgUrl, {
subjectIndex,
optionIndex
})
},
//选项图片上传成功回调
onOptionImgConfirm({img, ext}) {
this.$set(this.subjects[ext.subjectIndex].options[ext.optionIndex], "imgUrl", img)
},
//删除选项图片
removeOptionImg(subjectIndex, optionIndex){
this.$set(this.subjects[subjectIndex].options[optionIndex], "imgUrl", null)
console.log(this.subjects[subjectIndex])
},
//保存
saveQuestionnaire() {
this.$axios
.post("/platform/qsv/activity/saveSubjects", {
activityId: this.id,
subjects: JSON.stringify(this.subjects)
})
.then((res) => {
if (res.code === 0) {
this.visible = false
this.$message.success(res.msg)
this.$emit("refresh", null)
}
})
}
},
style: /*language=CSS*/ `
/*.subject-form-dialog,.subject-form-dialog .el-dialog__body {*/
/* background: rgb(248, 249, 250);*/
/*}*/
.questionnaire-editor {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.editor-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.subject-item {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 1px dashed #d9d9d9;
}
.subject-item:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.1);
/*transform: translateY(-1px);*/
}
.subject-header {
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 15px;
}
.subject-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #eee;
}
.toolbar {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.toolbar-btn {
margin: 5px 0;
width: 100%;
}
.option-item {
background: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
}
.option-drag-handle {
cursor: move;
margin-right: 10px;
color: #909399;
}
.option-content {
flex-grow: 1;
margin-right: 10px;
}
.option-tools {
display: flex;
align-items: center;
gap: 10px;
}
.preview-dialog {
max-width: 800px;
margin: 0 auto;
}
.date-visible {
margin-left: 20px;
}
.subject-meta {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
}
.image-preview {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
border-radius: 4px;
border: 1px solid #dcdfe6;
}
.drag-handler {
cursor: move;
color: #909399;
margin-right: 10px;
}
.meta-info {
color: #909399;
font-size: 12px;
margin-top: 5px;
}
.subject-type-icon {
margin-right: 5px;
}
.option-img-box{
position: relative;
width: 40px;
height: 40px;
cursor: pointer;
flex-shrink: 0
}
.option-img-box:hover .el-icon-remove {
display: block;
}
.option-img-box img{
width: 100%;
height: 100%;
}
.option-img-box .el-icon-remove{
position: absolute;
top: -7px;
right: -7px;
color: red;
display: none;
}
.option-img-box .el-icon-remove:hover{
transform: scale(1.1);
}
`
}
@@ -0,0 +1,111 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
:clearable="false"
@change="listActivity"
placeholder="选择年"
style="width: 100%"
type="year"
v-model="pageForm.year"
value-format="yyyy"
></el-date-picker>
</search-item>
<search-item label="问卷">
<el-select filterable placeholder="所属问卷" style="width: 100%" @change="doSearch" v-model="pageForm.activityId">
<el-option :label="item.title" :value="item.id" :key="item.id" v-for="item in activityOptions"></el-option>
</el-select>
</search-item>
<search-item label="姓名/工号">
<el-input v-model="pageForm.searchKeyword" placeholder="姓名/工号" clearable></el-input>
</search-item>
<search-item label="所属工会">
<el-select placeholder="所属工会" v-model="pageForm.unionId" clearable filterable>
<el-option v-for="item in unionOptions" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="所属单位">
<el-select placeholder="所属单位" v-model="pageForm.unitId" clearable filterable>
<el-option v-for="item in unitOptions" :label="item.name" :value="item.id"></el-option>
</el-select>
</search-item>
<search-item label="答题日期">
<el-date-picker
@change="doSearch"
style="width: 100%"
v-model="pageForm.attemptDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="选择答题日期"
></el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<el-button @click="exportXlsx" icon="el-icon-download" size="small" type="primary" class="mr5">导出xlsx</el-button>
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="姓名" prop="userName" sortable></el-table-column>
<el-table-column label="工号" prop="loginName" sortable></el-table-column>
<el-table-column label="性别" prop="sex" sortable></el-table-column>
<el-table-column label="单位" prop="unitName" sortable></el-table-column>
<el-table-column label="分工会" prop="unionName" sortable></el-table-column>
<el-table-column label="联系方式" prop="mobile" sortable></el-table-column>
<el-table-column label="答题日期" prop="attemptDate" sortable></el-table-column>
<el-table-column label="得分" prop="totalScore" sortable></el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</div>
<script>
new Vue({
el: "#app",
mixins: [initTableMixins],
data() {
return {
activityOptions: [],
unionOptions: [],
unitOptions: []
}
},
methods: {
listActivity() {
this.$axios.post("/platform/qsv/quizRank/listQuiz", { year: this.pageForm.year }).then((res) => {
if (res.code === 0) {
this.activityOptions = res.data
if (this.activityOptions.length > 0) {
this.pageForm.activityId = this.activityOptions[0].id
this.pageData()
} else {
this.pageForm.activityId = null
this.tableData = []
this.pageForm.totalCount = 0
this.pageForm.pageNumber = 1
}
}
})
},
exportXlsx() {
this.$downLoad("/platform/qsv/quizRank/exportXlsx", this.pageForm)
}
},
async created() {
this.unionOptions = await this.$businessTool.listUnion()
this.unitOptions = await this.$businessTool.listUnit()
this.listActivity()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,62 @@
const answer = {
/*language=HTML*/
template: `
<el-dialog title="查看答卷" :visible.sync="dialogVisible" width="50%">
<el-row type="flex" justify="end" class="mb10">
<el-button type="primary" size="small" icon="el-icon-download" @click="exportUserAnswerXlsx">导出xlsx</el-button>
</el-row>
<el-table :data="tableData">
<el-table-column label="序号" type="index" width="70"></el-table-column>
<el-table-column v-for="item in tableColumns" :key="item.prop" :label="item.label" :prop="item.prop"></el-table-column>
<el-table-column label="操作" fixed="right" width="100px">
<template slot-scope="scope">
<el-button type="danger" size="mini" icon="el-icon-delete" @click="onDelete(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
`,
data() {
return {
dialogVisible: false,
tableColumns: [],
tableData: [],
activityId: null
}
},
methods: {
onOpen(activityId) {
this.activityId = activityId
this.dialogVisible = true
this.list()
},
list() {
this.$axios.post("/platform/qsv/survey/userAnswer", { activityId: this.activityId }).then((res) => {
if (res.code === 0) {
this.tableColumns = res.data.tableColumns
this.tableData = res.data.tableData
}
})
},
onDelete(id) {
this.$confirm("确定要删除吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$axios.post("/platform/qsv/survey/deleteUserAnswer", { id }).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.list()
}
})
})
},
exportUserAnswerXlsx() {
this.$downLoad("/platform/qsv/survey/exportUserAnswerXlsx", { activityId: this.activityId })
}
}
}
@@ -0,0 +1,70 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度">
<el-date-picker
:clearable="false"
placeholder="选择年"
style="width: 100%"
type="year"
v-model="pageForm.year"
value-format="yyyy"
></el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never">
<table-tool>
<!-- <el-button @click="exportXlsx" icon="el-icon-download" size="small" type="primary" class="mr5">导出xlsx</el-button>-->
</table-tool>
<el-table :data="tableData" ref="tableRef" @sort-change="pageOrder">
<el-table-column label="序号" width="50" type="index" :index="indexMethod"></el-table-column>
<el-table-column label="标题" prop="title" width="120" sortable></el-table-column>
<el-table-column label="类型" prop="category" sortable>
<template scope="{row}">
<dict-tag :options="dict.type.ACTIVITY_QSV_CATEGORY" :value="row.category"></dict-tag>
</template>
</el-table-column>
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="200px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="$refs.reportRef.onOpen(row.id)">查看分析</el-button>
<el-button size="mini" type="primary" @click="$refs.answerRef.onOpen(row.id)">查看答卷</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<report ref="reportRef"></report>
<answer ref="answerRef"></answer>
</div>
<script>
<!--#include('report.js'){}#-->
<!--#include('answer.js'){}#-->
new Vue({
el: "#app",
dicts: ["ACTIVITY_QSV_CATEGORY"],
components: { report, answer },
mixins: [initTableMixins],
data() {
return {}
},
methods: {
exportXlsx() {
this.$axios.post("/platform/qsv/survey/exportXlsx", this.pageForm)
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,101 @@
const report = {
/*language=HTML*/
template: `
<el-dialog title="分析报告" :visible.sync="dialogVisible" width="50%">
<div v-for="(subject,subjectIndex) in subjects" class="subject">
<div>
<div class="title">
第{{subjectIndex+1}}题: {{subject.title}}
<span class="type" v-if="subject.type==='text'">[填空题]</span>
<span class="type" v-else-if="subject.type==='radio'">[单选题]</span>
<span class="type" v-else-if="subject.type==='text'">[多选题]</span>
</div>
<div v-if="subject.type==='text'">
{{subject.texts.join('、')}}
</div>
<div v-else-if="subject.type==='radio' || subject.type==='checkbox'">
<el-table :data="subject.options" size="small">
<el-table-column label="选项" prop="text"></el-table-column>
<el-table-column label="小计" prop="selectCount" width="100"></el-table-column>
<el-table-column label="比例" width="500">
<template slot-scope="scope">
<el-progress
:percentage="Math.round(scope.row.selectCount / subject.selectTotal * 100)"></el-progress>
</template>
</el-table-column>
<el-table-column label="详情" width="100px">
<template slot-scope="{row}">
<el-button type="text" @click="openDetail(subject.id,row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<el-dialog title="选择详情" :visible.sync="optionVisible" width="50%" append-to-body>
<el-table :data="optionUsers" size="small">
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="单位" prop="unitName"></el-table-column>
<el-table-column label="分工会" prop="unionName"></el-table-column>
<el-table-column label="选择时间" prop="attemptDate"></el-table-column>
</el-table>
</el-dialog>
</el-dialog>
`,
dicts: ["ACTIVITY_QSV_CATEGORY"],
data() {
return {
dialogVisible: false,
activityId: null,
report: null,
subjects: [],
optionVisible: false,
optionUsers: []
}
},
methods: {
onOpen(activityId) {
this.dialogVisible = true
this.activityId = activityId
this.$axios.post("/platform/qsv/survey/report", { activityId }).then((res) => {
if (res.code === 0) {
this.subjects = res.data
}
})
},
openDetail(subjectId, optionId) {
console.log(subjectId)
console.log(optionId)
this.$axios
.post("/platform/qsv/survey/selectOptionUsers", {
activityId: this.activityId,
subjectId,
optionId
})
.then((res) => {
if (res.code === 0) {
this.optionVisible = true
this.optionUsers = res.data
}
})
}
},
style: /*language=CSS*/ `
.subject{
margin-bottom: 20px;
}
.subject .title{
margin-bottom: 5px;
}
.subject .type{
color: #a6a6a6;
margin-left: 10px;
}
`
}