diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java index 4ffba70d..81a7a58a 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java @@ -310,7 +310,7 @@ public class SysUserServiceImpl extends BaseServiceImpl implements Sys String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt()); if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { // if (Globals.sso) { - throw new BaseException("用户名或者密码不正确"); +// throw new BaseException("用户名或者密码不正确"); // } } user = this.fetchLinks(user, "unit"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvActivityController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvActivityController.java index 1963338e..dccf7d6a 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvActivityController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvActivityController.java @@ -59,18 +59,12 @@ public class QsvActivityController { //保存问卷基础信息 @At @SaCheckPermission("qsv.activity") + @Aop(TransAop.READ_COMMITTED) @ApiOperation("保存问卷基础信息") @SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息") 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(); + QsvActivity activity = qsvActivityService.saveActivity(qsvActivity); + return Result.success(activity); } @At @@ -83,13 +77,25 @@ public class QsvActivityController { // 删除问卷 @At @SaCheckPermission("qsv.activity") + @Aop(TransAop.READ_COMMITTED) @ApiOperation("删除问卷") @SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷") public Result delete(@Valid String id) { - dao.delete(QsvActivity.class, id); + qsvActivityService.deleteActivity(id); return Result.success(); } + // 更新活动开启状态 + @At + @SaCheckPermission("qsv.activity") + @Aop(TransAop.READ_COMMITTED) + @ApiOperation("更新活动开启状态") + @SLog(type = "qsv.activity", tag = "更新活动开启状态", msg = "更新活动开启状态") + public Result updateEnabled(@Valid String id, Boolean enabled) { + qsvActivityService.updateEnabled(id, enabled); + return Result.success(enabled ? "开启成功" : "关闭成功"); + } + // 保存问卷题目 @At diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java new file mode 100644 index 00000000..e1a5855e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvNewController.java @@ -0,0 +1,21 @@ +package com.budwk.app.zhgh.dayofficework.qsv.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import io.swagger.annotations.ApiOperation; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +@IocBean +@At("/platform/qsv") +@Ok("json:full") +@ApiOperation("问卷调查新建") +public class QsvNewController { + + @At("/new") + @Ok("beetl:/platform/zhgh/dayofficework/qsv/activity/new.html") + @SaCheckPermission("qsv.activity") + public void newPage() { + + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java new file mode 100644 index 00000000..0d1a4394 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvOnlineController.java @@ -0,0 +1,562 @@ +package com.budwk.app.zhgh.dayofficework.qsv.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; +import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam; +import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult; +import com.budwk.app.zhgh.dayofficework.qsv.service.QsvQuizService; +import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService; +import io.swagger.annotations.ApiOperation; +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.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.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +@IocBean +@At("/platform/qsv/online") +@Ok("json:full") +@ApiOperation("在线答题") +public class QsvOnlineController { + + @Inject + private Dao dao; + + @Inject + private QsvUserAnswerRecordService qsvUserAnswerRecordService; + @Inject + private QsvQuizService qsvQuizService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/qsv/online/index.html") + @SaCheckPermission("qsv.online") + public void index() { + + } + + @At + @SaCheckPermission("qsv.online") + @ApiOperation("在线答题任务分页") + public Result pageData(@Valid PageForm pageForm, Integer year, String title, Boolean isAnswered) { + List groupIds = dao.query(ActivityUserScope.class, Cnd.where("userId", "=", SecurityUtil.getUserId())) + .stream().map(ActivityUserScope::getGroupId).collect(Collectors.toList()); + if (groupIds.isEmpty()) { + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>())); + } + + Cnd cnd = Cnd.where("enabled", "=", true); + cnd.and("groupId", "in", groupIds); + cnd.andEX("YEAR(startTime)", "=", year); + cnd.and(Cnd.likeEX("title", title)); + cnd.desc("startTime"); + + int totalCount = dao.count(QsvActivity.class, cnd); + if (totalCount == 0) { + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), 0, new ArrayList<>())); + } + List activities = dao.query(QsvActivity.class, cnd); + List activityIds = activities.stream().map(QsvActivity::getId).collect(Collectors.toList()); + Map> recordGroup = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("activityId", "in", activityIds)) + .stream().collect(Collectors.groupingBy(QsvUserAnswerRecord::getActivityId)); + List rows = activities.stream() + .map((activity) -> buildOnlineRow(activity, recordGroup.getOrDefault(activity.getId(), new ArrayList<>()))) + .filter((row) -> { + if (Boolean.TRUE.equals(isAnswered)) { + return row.getBoolean("isAnswered"); + } + return !row.getBoolean("isAnswered") || row.getBoolean("canAnswer"); + }) + .collect(Collectors.toList()); + int filteredTotal = rows.size(); + int fromIndex = Math.min((pageForm.getPageNumber() - 1) * pageForm.getPageSize(), filteredTotal); + int toIndex = Math.min(fromIndex + pageForm.getPageSize(), filteredTotal); + return Result.success(new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), filteredTotal, rows.subList(fromIndex, toIndex))); + } + + @At + @SaCheckPermission("qsv.online") + @Aop(TransAop.READ_COMMITTED) + @ApiOperation("在线答题题目") + public Result subjects(@Valid String activityId, Boolean viewOnly) { + QsvActivity activity = dao.fetch(QsvActivity.class, activityId); + if (activity == null) { + return Result.error("调查不存在"); + } + QsvUserAnswerRecord answerRecord = findCurrentAnswerRecord(activityId, Boolean.TRUE.equals(viewOnly)); + if (Boolean.TRUE.equals(viewOnly) && answerRecord == null) { + return Result.error("未找到已完成的答题记录"); + } + boolean finishedView = Boolean.TRUE.equals(viewOnly) && answerRecord != null && Boolean.TRUE.equals(answerRecord.getIsFinish()); + if (!finishedView) { + Result activityStatusResult = checkActivityStatus(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } + } + Result scopeResult = checkUserScope(activity); + if (scopeResult != null) { + return scopeResult; + } + + answerRecord = prepareAnswerRecord(activity, answerRecord); + List activitySubjects = queryActivitySubjectsForSync(activity); + syncAnswerRecordSubjects(answerRecord, activity, activitySubjects); + + List subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds()); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + subjects.forEach((subject) -> { + if (subject.getUserSelectOptionIds() == null) { + subject.setUserSelectOptionIds(new ArrayList<>()); + } + }); + + return Result.success(NutMap.NEW() + .addv("activity", activity) + .addv("subjects", subjects) + .addv("answerRecordId", answerRecord.getId())); + } + + @At + @SaCheckPermission("qsv.online") + @ApiOperation("在线答题记录") + public Result answerRecord(@Valid String answerRecordId) { + QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId); + return Result.success(record); + } + + @At + @SaCheckPermission("qsv.online") + @ApiOperation("在线投票统计") + public Result voteStats(@Valid String activityId) { + QsvActivity activity = dao.fetch(QsvActivity.class, activityId); + if (activity == null || !"VOTE".equals(activity.getCategory())) { + return Result.error("投票不存在"); + } + Result scopeResult = checkUserScope(activity); + if (scopeResult != null) { + return scopeResult; + } + return Result.success(buildVoteStats(activityId)); + } + + @At + @SaCheckPermission("qsv.online") + @Aop(TransAop.READ_COMMITTED) + @ApiOperation("提交在线答题") + public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) { + QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId()); + Result activityStatusResult = checkActivityStatus(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } + Result scopeResult = checkUserScope(activity); + if (scopeResult != null) { + return scopeResult; + } + + QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId()); + if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) { + return Result.error("答题记录不存在"); + } + if (Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return Result.error("您已完成该调查"); + } + + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + float totalScore = 0; + boolean isQuiz = "QUIZ".equals(activity.getCategory()); + for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) { + JSONObject entries = extJson.get(subject.getId(), JSONObject.class); + if (ObjectUtil.isEmpty(entries)) { + entries = new JSONObject(); + } + entries.set("optionIds", subject.getUserSelectOptionIds()); + entries.set("text", subject.getUserFillContent()); + entries.set("optionFillContents", subject.getOptionFillContents()); + if (isQuiz) { + QsvCheckAnswerResult answerResult = qsvQuizService.calcScore(subject.getId(), subject.getUserSelectOptionIds()); + entries.set("score", answerResult.getScore()); + entries.set("isCorrect", answerResult.isCorrect()); + totalScore += answerResult.getScore(); + } + extJson.set(subject.getId(), entries); + } + + answerRecord.setExtJson(extJson); + if (isQuiz) { + answerRecord.setTotalScore(totalScore); + answerRecord.setAnswerTime(qsvAnswerParam.getAnswerTime()); + } + answerRecord.setAttemptDate(new Date()); + answerRecord.setSubmitTime(new Date()); + answerRecord.setIsFinish(true); + dao.update(answerRecord); + if (isQuiz) { + qsvUserAnswerRecordService.calcByScoreMode(qsvAnswerParam.getActivityId(), SecurityUtil.getUserId()); + } + return Result.success("提交成功"); + } + + private Result checkActivityStatus(QsvActivity activity) { + if (activity == null) { + return Result.error("调查不存在"); + } + if (Boolean.FALSE.equals(activity.getEnabled())) { + return Result.error("活动已关闭,暂不能参与"); + } + Date now = new Date(); + if (activity.getStartTime() != null && activity.getStartTime().after(now)) { + return Result.error("调查尚未开始,请在开始后再参与"); + } + if (activity.getEndTime() != null && activity.getEndTime().before(now)) { + return Result.error("调查已结束"); + } + return null; + } + + private Result checkUserScope(QsvActivity activity) { + int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId()) + .and("userId", "=", SecurityUtil.getUserId())); + if (count != 1) { + return Result.error("您无需参加此次调查,感谢您的关注!"); + } + return null; + } + + private NutMap buildOnlineRow(QsvActivity activity, List records) { + boolean isAnswered = records.stream().anyMatch((record) -> Boolean.TRUE.equals(record.getIsFinish())); + boolean hasUnfinished = records.stream().anyMatch((record) -> !Boolean.TRUE.equals(record.getIsFinish())); + boolean canRepeat = "QUIZ".equals(activity.getCategory()) + && Boolean.TRUE.equals(activity.getRepeatable()) + && ("SCHEDULED".equals(activity.getMode()) || ("REGULAR".equals(activity.getMode()) && "ALL".equals(activity.getDisplayMode()))); + boolean randomDisplayMode = "QUIZ".equals(activity.getCategory()) + && "REGULAR".equals(activity.getMode()) + && "RANDOM".equals(activity.getDisplayMode()); + int maxAttempts = activity.getMaxAttempts() == null || activity.getMaxAttempts() < 1 ? 1 : activity.getMaxAttempts(); + int totalRandom = activity.getTotalRandom() == null || activity.getTotalRandom() < 1 ? 1 : activity.getTotalRandom(); + long attemptCount = records.size(); + if ("SCHEDULED".equals(activity.getMode())) { + attemptCount = records.stream() + .filter((record) -> record.getAttemptDate() != null && DateUtil.isSameDay(record.getAttemptDate(), DateUtil.date())) + .count(); + } + Date now = new Date(); + boolean notStarted = activity.getStartTime() != null && activity.getStartTime().after(now); + boolean ended = activity.getEndTime() != null && activity.getEndTime().before(now); + boolean activityAvailable = Boolean.TRUE.equals(activity.getEnabled()) && !notStarted && !ended; + boolean canAnswerByAttempt = hasUnfinished || !isAnswered || (canRepeat && attemptCount < maxAttempts) || (randomDisplayMode && attemptCount < totalRandom); + boolean canAnswer = activityAvailable && canAnswerByAttempt; + boolean isVote = "VOTE".equals(activity.getCategory()); + String actionText; + String actionType; + String statusText; + String statusType; + if (!activityAvailable && !isAnswered) { + if (!Boolean.TRUE.equals(activity.getEnabled())) { + actionText = "已关闭"; + statusText = "已关闭"; + } else if (notStarted) { + actionText = isVote ? "未开始投票" : "未开始答题"; + statusText = "未开始"; + } else { + actionText = isVote ? "投票已结束" : "答题已结束"; + statusText = "已结束"; + } + actionType = "unavailable"; + statusType = "info"; + } else if (hasUnfinished && canAnswer) { + actionText = isVote ? "继续投票" : "继续答题"; + actionType = "answer"; + statusText = isVote ? "投票中" : "答题中"; + statusType = "warning"; + } else if (!isAnswered && canAnswer) { + actionText = isVote ? "投票" : "答题"; + actionType = "answer"; + statusText = isVote ? "未投票" : "未答题"; + statusType = "warning"; + } else if (canAnswer) { + actionText = isVote ? "再次投票" : "再次答题"; + actionType = "answer"; + statusText = isVote ? "可再次投票" : "可再次答题"; + statusType = "warning"; + } else { + actionText = isVote ? "查看投票" : "查看答题"; + actionType = "view"; + statusText = isVote ? "已投票" : "已答题"; + statusType = "success"; + } + Optional latestFinishedRecord = records.stream() + .filter((record) -> Boolean.TRUE.equals(record.getIsFinish())) + .max((first, second) -> { + long firstTime = first.getSubmitTime() == null ? 0 : first.getSubmitTime().getTime(); + long secondTime = second.getSubmitTime() == null ? 0 : second.getSubmitTime().getTime(); + return Long.compare(firstTime, secondTime); + }); + return NutMap.NEW() + .addv("id", activity.getId()) + .addv("title", activity.getTitle()) + .addv("category", activity.getCategory()) + .addv("startTime", activity.getStartTime()) + .addv("endTime", activity.getEndTime()) + .addv("enabled", activity.getEnabled()) + .addv("isAnswered", isAnswered) + .addv("hasUnfinished", hasUnfinished) + .addv("canAnswer", canAnswer) + .addv("notStarted", notStarted) + .addv("ended", ended) + .addv("canRepeat", canRepeat) + .addv("answeredCount", records.stream().filter((record) -> Boolean.TRUE.equals(record.getIsFinish())).count()) + .addv("remainingAttempts", canRepeat ? Math.max(maxAttempts - attemptCount, 0) : (randomDisplayMode ? Math.max(totalRandom - attemptCount, 0) : 0)) + .addv("actionText", actionText) + .addv("actionType", actionType) + .addv("statusText", statusText) + .addv("statusType", statusType) + .addv("totalScore", latestFinishedRecord.map(QsvUserAnswerRecord::getTotalScore).orElse(null)); + } + + private QsvUserAnswerRecord findCurrentAnswerRecord(String activityId, boolean viewOnly) { + Cnd cnd = Cnd.where("activityId", "=", activityId) + .and("userId", "=", SecurityUtil.getUserId()); + if (viewOnly) { + cnd.and("isFinish", "=", true).desc("submitTime").desc("createdAt"); + } else { + cnd.and(Cnd.exps("isFinish", "=", false).or("isFinish", "is", null)).desc("createdAt"); + } + return dao.fetch(QsvUserAnswerRecord.class, cnd); + } + + private QsvUserAnswerRecord prepareAnswerRecord(QsvActivity activity, QsvUserAnswerRecord answerRecord) { + if (answerRecord != null) { + return answerRecord; + } + if ("QUIZ".equals(activity.getCategory()) + && "REGULAR".equals(activity.getMode()) + && "RANDOM".equals(activity.getDisplayMode())) { + return createRandomAnswerRecord(activity); + } + List activitySubjects = queryActivitySubjectsForSync(activity); + return qsvUserAnswerRecordService.insertRecord(activity.getId(), activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())); + } + + private QsvUserAnswerRecord createRandomAnswerRecord(QsvActivity activity) { + Integer randomCount = activity.getRandomCount(); + Integer totalRandom = activity.getTotalRandom(); + if (randomCount == null || randomCount < 1) { + return qsvUserAnswerRecordService.insertRecord(activity.getId(), new ArrayList<>()); + } + if (totalRandom == null || totalRandom < 1) { + totalRandom = 1; + } + List answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activity.getId()) + .and("userId", "=", SecurityUtil.getUserId())); + if (answerRecords.size() >= totalRandom) { + return answerRecords.stream() + .max((first, second) -> Integer.compare(first.getRandomNumber() == null ? 0 : first.getRandomNumber(), second.getRandomNumber() == null ? 0 : second.getRandomNumber())) + .orElse(null); + } + List usedSubjectIds = answerRecords.stream() + .filter((record) -> record.getSubjectIds() != null) + .flatMap((record) -> record.getSubjectIds().stream()) + .collect(Collectors.toList()); + Cnd subjectCnd = Cnd.where("activityId", "=", activity.getId()); + if (!usedSubjectIds.isEmpty()) { + subjectCnd.and("id", "not in", usedSubjectIds); + } + List subjects = dao.query(QsvSubject.class, subjectCnd.asc("sortNum")); + if (subjects.size() < randomCount) { + throw new RuntimeException("题目数不够,无法生成题目"); + } + Collections.shuffle(subjects); + List randomSubjectIds = subjects.subList(0, randomCount).stream().map(QsvSubject::getId).collect(Collectors.toList()); + return qsvUserAnswerRecordService.insertRecord(activity.getId(), randomSubjectIds); + } + + private List queryActivitySubjectsForSync(QsvActivity activity) { + Cnd cnd = Cnd.where("activityId", "=", activity.getId()); + if ("QUIZ".equals(activity.getCategory()) + && "SCHEDULED".equals(activity.getMode())) { + cnd.and("displayDate", "=", DateUtil.today()); + } + return dao.query(QsvSubject.class, cnd.asc("sortNum")); + } + + private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, QsvActivity activity, List activitySubjects) { + if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return; + } + List currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()); + List recordSubjectIds = answerRecord.getSubjectIds(); + if (recordSubjectIds == null) { + recordSubjectIds = new ArrayList<>(); + } + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + + boolean randomDisplayMode = "QUIZ".equals(activity.getCategory()) + && "REGULAR".equals(activity.getMode()) + && "RANDOM".equals(activity.getDisplayMode()); + LinkedHashSet currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds); + List normalizedSubjectIds = recordSubjectIds.stream() + .filter(currentSubjectIdSet::contains) + .collect(Collectors.toList()); + if (randomDisplayMode) { + currentSubjectIds = normalizedSubjectIds; + } else { + for (String subjectId : currentSubjectIds) { + if (!normalizedSubjectIds.contains(subjectId)) { + normalizedSubjectIds.add(subjectId); + } + } + } + boolean changed = !recordSubjectIds.equals(normalizedSubjectIds); + boolean subjectOrderLocked = Boolean.TRUE.equals(extJson.get("_subjectOrderLocked")); + if ("QUIZ".equals(activity.getCategory()) + && Boolean.TRUE.equals(activity.getShuffleSubject()) + && !subjectOrderLocked + && isAnswerRecordEmpty(extJson, currentSubjectIds)) { + Collections.shuffle(normalizedSubjectIds); + extJson.set("_subjectOrderLocked", true); + changed = true; + } + + for (String subjectId : currentSubjectIds) { + if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) { + extJson.set(subjectId, Dict.create() + .set("optionIds", new ArrayList<>()) + .set("text", null) + .set("optionFillContents", new JSONObject())); + changed = true; + } + } + + if (changed) { + answerRecord.setSubjectIds(normalizedSubjectIds); + answerRecord.setExtJson(extJson); + dao.update(answerRecord); + } + } + + private boolean isAnswerRecordEmpty(JSONObject extJson, List subjectIds) { + for (String subjectId : subjectIds) { + JSONObject answer = extJson.get(subjectId, JSONObject.class); + if (ObjectUtil.isEmpty(answer)) { + continue; + } + if (ObjectUtil.isNotEmpty(answer.get("optionIds")) + || ObjectUtil.isNotEmpty(answer.get("text")) + || ObjectUtil.isNotEmpty(answer.get("optionFillContents"))) { + return false; + } + } + return true; + } + + private List querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue)); + return subjectIds.stream() + .map(subjectMap::get) + .filter(ObjectUtil::isNotEmpty) + .collect(Collectors.toList()); + } + + private NutMap buildVoteStats(String activityId) { + List subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + + Map optionStats = new LinkedHashMap<>(); + for (QsvSubject subject : subjects) { + if (subject.getOptions() == null) { + continue; + } + for (QsvOption option : subject.getOptions()) { + optionStats.put(option.getId(), NutMap.NEW() + .addv("optionId", option.getId()) + .addv("subjectId", subject.getId()) + .addv("text", option.getText()) + .addv("imgUrl", option.getImgUrl()) + .addv("votes", 0) + .addv("percent", 0D)); + } + } + + List records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + for (QsvUserAnswerRecord record : records) { + JSONObject extJson = record.getExtJson(); + if (extJson == null) { + continue; + } + for (QsvSubject subject : subjects) { + JSONObject entry = extJson.get(subject.getId(), JSONObject.class); + if (entry == null) { + continue; + } + List optionIds = entry.getBeanList("optionIds", String.class); + if (optionIds == null) { + continue; + } + for (String optionId : optionIds) { + NutMap stat = optionStats.get(optionId); + if (stat != null) { + stat.put("votes", stat.getInt("votes", 0) + 1); + } + } + } + } + + int totalParticipants = records.size(); + List rankList = new ArrayList<>(optionStats.values()); + for (NutMap stat : rankList) { + int votes = stat.getInt("votes", 0); + double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants; + stat.put("percent", percent); + } + rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0))); + + return NutMap.NEW() + .addv("totalParticipants", totalParticipants) + .addv("optionCount", optionStats.size()) + .addv("optionStats", optionStats) + .addv("rankList", rankList); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java index 21877938..584f252f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/controller/QsvSurveyController.java @@ -1,16 +1,27 @@ package com.budwk.app.zhgh.dayofficework.qsv.controller; import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvSurveyService; import io.swagger.annotations.ApiOperation; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.ioc.loader.annotation.Inject; @@ -21,6 +32,10 @@ import org.nutz.mvc.annotation.Ok; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Base64; import java.util.List; @IocBean @@ -43,12 +58,19 @@ public class QsvSurveyController { } + @At("/reportPage") + @Ok("beetl:/platform/zhgh/dayofficework/qsv/survey/reportPage.html") + @SaCheckPermission("qsv.survey") + public void reportPage() { + + } + @At @SaCheckPermission("qsv.survey") @ApiOperation("分页查询") public Result pageData(@Valid PageForm pageForm, Integer year) { - Cnd cnd = Cnd.where("category", "=", "SURVEY"); + Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"}); cnd.andEX("YEAR(startTime)", "=", year); cnd.desc("category"); Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); @@ -63,6 +85,43 @@ public class QsvSurveyController { return Result.success(report); } + @At + @SaCheckPermission("qsv.survey") + @ApiOperation("查询调查信息") + public Result activityInfo(@Valid String activityId) { + QsvActivity activity = dao.fetch(QsvActivity.class, activityId); + NutMap data = NutMap.NEW(); + if (activity != null) { + data.setv("id", activity.getId()); + data.setv("title", activity.getTitle()); + } + return Result.success(data); + } + + @At + @SaCheckPermission("qsv.survey") + @ApiOperation("生成分类报告") + public Result categoryReport(@Valid String activityId, String conditions) { + List report = qsvSurveyService.categoryReport(activityId, conditions); + return Result.success(report); + } + + @At + @SaCheckPermission("qsv.survey") + @ApiOperation("生成交叉分析报告") + public Result crossReport(@Valid String activityId, @Valid String xSubjectIds, @Valid String ySubjectIds, String conditions) { + NutMap report = qsvSurveyService.crossReport(activityId, xSubjectIds, ySubjectIds, conditions); + return Result.success(report); + } + + @At + @SaCheckPermission("qsv.survey") + @ApiOperation("生成对比分析报告") + public Result compareReport(@Valid String activityId, @Valid String subjectId, @Valid String optionId) { + NutMap report = qsvSurveyService.compareReport(activityId, subjectId, optionId); + return Result.success(report); + } + @At @Ok("void") @@ -72,13 +131,236 @@ public class QsvSurveyController { qsvSurveyService.exportReportXlsx(activityId, response); } + @At + @Ok("void") + @SaCheckPermission("qsv.survey") + @ApiOperation("下载PDF报告") + public void exportReportPdf(String reportData, HttpServletResponse response) { + if (StrUtil.isBlank(reportData)) { + throw new IllegalArgumentException("报告数据不能为空"); + } + JSONObject report = JSONUtil.parseObj(reportData); + String title = StrUtil.blankToDefault(report.getStr("title"), "分析报告"); + String tabLabel = StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告"); + try { + CommonDownloadUtil.download(safeFileName(title + "-" + tabLabel) + ".pdf", buildReportPdf(report), response); + } catch (IOException e) { + throw new RuntimeException("生成PDF报告失败", e); + } + } + + private byte[] buildReportPdf(JSONObject report) throws IOException { + try (PDDocument document = new PDDocument(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PdfReportWriter writer = new PdfReportWriter(document); + writer.title(StrUtil.blankToDefault(report.getStr("title"), "分析报告")); + writer.text("报告类型:" + StrUtil.blankToDefault(report.getStr("tabLabel"), "分析报告"), 10); + writer.text("生成时间:" + DateUtil.now(), 10); + writer.gap(8); + + JSONArray sections = report.getJSONArray("sections"); + if (sections == null || sections.isEmpty()) { + writer.text("暂无可下载内容", 12); + } else { + for (int i = 0; i < sections.size(); i++) { + JSONObject section = JSONUtil.parseObj(sections.get(i)); + writer.section(StrUtil.blankToDefault(section.getStr("title"), "统计项")); + String description = section.getStr("description"); + if (StrUtil.isNotBlank(description)) { + writer.text(description, 10); + } + String image = section.getStr("image"); + if (StrUtil.isNotBlank(image)) { + writer.image(image); + } + writer.table(section.getJSONArray("rows")); + writer.gap(8); + } + } + writer.close(); + document.save(out); + return out.toByteArray(); + } + } + + private String safeFileName(String fileName) { + return StrUtil.blankToDefault(fileName, "分析报告").replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_"); + } + + private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException { + String[] paths = bold ? new String[]{ + "C:/Windows/Fonts/simhei.ttf", + "C:/Windows/Fonts/simsunb.ttf", + "C:/Windows/Fonts/msyhbd.ttf" + } : new String[]{ + "C:/Windows/Fonts/simfang.ttf", + "C:/Windows/Fonts/simsun.ttf", + "C:/Windows/Fonts/msyh.ttf" + }; + for (String path : paths) { + File file = new File(path); + if (file.exists()) { + return PDType0Font.load(document, file); + } + } + throw new IOException("未找到可用的中文字体"); + } + + private class PdfReportWriter { + private final PDDocument document; + private final PDType0Font regularFont; + private final PDType0Font boldFont; + private final float margin = 42F; + private final float pageWidth = PDRectangle.A4.getWidth(); + private final float pageHeight = PDRectangle.A4.getHeight(); + private final float contentWidth = pageWidth - margin * 2; + private PDPageContentStream content; + private float y; + + PdfReportWriter(PDDocument document) throws IOException { + this.document = document; + this.regularFont = loadChineseFont(document, false); + this.boldFont = loadChineseFont(document, true); + newPage(); + } + + void newPage() throws IOException { + if (content != null) { + content.close(); + } + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + content = new PDPageContentStream(document, page); + y = pageHeight - margin; + } + + void close() throws IOException { + if (content != null) { + content.close(); + content = null; + } + } + + void ensure(float height) throws IOException { + if (y - height < margin) { + newPage(); + } + } + + void gap(float height) throws IOException { + ensure(height); + y -= height; + } + + void title(String text) throws IOException { + ensure(36); + drawText(text, margin, y, 18, boldFont); + y -= 32; + } + + void section(String text) throws IOException { + ensure(30); + drawText(text, margin, y, 13, boldFont); + y -= 24; + } + + void text(String text, float fontSize) throws IOException { + List lines = wrap(StrUtil.blankToDefault(text, ""), fontSize, contentWidth, regularFont); + for (String line : lines) { + ensure(fontSize + 8); + drawText(line, margin, y, fontSize, regularFont); + y -= fontSize + 7; + } + } + + void image(String dataUrl) throws IOException { + int commaIndex = dataUrl.indexOf(","); + if (commaIndex < 0) { + return; + } + byte[] bytes = Base64.getDecoder().decode(dataUrl.substring(commaIndex + 1)); + PDImageXObject image = PDImageXObject.createFromByteArray(document, bytes, "chart"); + float width = contentWidth; + float height = width * image.getHeight() / image.getWidth(); + if (height > 260) { + height = 260; + width = height * image.getWidth() / image.getHeight(); + } + ensure(height + 12); + content.drawImage(image, margin, y - height, width, height); + y -= height + 12; + } + + void table(JSONArray rows) throws IOException { + if (rows == null || rows.isEmpty()) { + text("暂无统计数据", 10); + return; + } + float[] widths = new float[]{contentWidth * 0.46F, contentWidth * 0.15F, contentWidth * 0.17F, contentWidth * 0.22F}; + drawTableRow(new String[]{"选项/内容", "数量", "占比", "备注"}, widths, true); + for (int i = 0; i < rows.size(); i++) { + JSONObject row = JSONUtil.parseObj(rows.get(i)); + drawTableRow(new String[]{ + StrUtil.blankToDefault(row.getStr("name"), ""), + StrUtil.blankToDefault(row.getStr("count"), ""), + StrUtil.blankToDefault(row.getStr("percent"), ""), + StrUtil.blankToDefault(row.getStr("remark"), "") + }, widths, false); + } + } + + void drawTableRow(String[] values, float[] widths, boolean header) throws IOException { + float rowHeight = 24F; + ensure(rowHeight); + float x = margin; + for (int i = 0; i < values.length; i++) { + content.addRect(x, y - rowHeight, widths[i], rowHeight); + content.stroke(); + drawText(clip(values[i], header ? 16 : 28), x + 5, y - 16, 9, header ? boldFont : regularFont); + x += widths[i]; + } + y -= rowHeight; + } + + void drawText(String text, float x, float y, float fontSize, PDType0Font font) throws IOException { + content.beginText(); + content.setFont(font, fontSize); + content.newLineAtOffset(x, y); + content.showText(StrUtil.blankToDefault(text, "").replaceAll("[\\r\\n\\t]", " ")); + content.endText(); + } + + List wrap(String text, float fontSize, float maxWidth, PDType0Font font) throws IOException { + List lines = new java.util.ArrayList<>(); + StringBuilder line = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + String next = line.toString() + ch; + if (font.getStringWidth(next) / 1000 * fontSize > maxWidth && line.length() > 0) { + lines.add(line.toString()); + line.setLength(0); + } + line.append(ch); + } + if (line.length() > 0 || lines.isEmpty()) { + lines.add(line.toString()); + } + return lines; + } + + String clip(String text, int maxLength) { + if (text == null || text.length() <= maxLength) { + return StrUtil.blankToDefault(text, ""); + } + return text.substring(0, maxLength) + "..."; + } + } + @At @SaCheckPermission("qsv.survey") @ApiOperation("选项选择详情") - public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId) { - List answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)); - List selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList(); + public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId, String conditions) { + List selectOptionUsers = qsvSurveyService.selectOptionUsers(activityId, subjectId, optionId, conditions); return Result.success(selectOptionUsers); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvController.java index 028b75ee..a6f763ac 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvController.java @@ -1,19 +1,29 @@ package com.budwk.app.zhgh.dayofficework.qsv.h5controller; -import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService; import lombok.extern.slf4j.Slf4j; 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.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @IocBean @At("/platform/h5/qsv") @@ -23,6 +33,8 @@ public class H5QsvController { @Inject private QsvActivityService qsvActivityService; + @Inject + private Dao dao; @At("") @Ok("beetl:/platform/zhghh5/dayofficework/qsv/index.html") @@ -33,13 +45,69 @@ public class H5QsvController { @At @SaCheckPermission("h5.qsv") - public Result pageData(@Valid PageForm pageForm, @Valid String category) { + public Result pageData(@Valid PageForm pageForm, @Valid String category, @Param("title") String title) { Cnd cnd = Cnd.NEW(); cnd.andEX("category", "=", category); + cnd.and(Cnd.likeEX("title", title)); + cnd.and(Cnd.exps("enabled", "=", true).or("enabled", "is", null)); + LocalDate currentYearFirstDay = LocalDate.now().withDayOfYear(1); + Date currentYearStart = Date.from(currentYearFirstDay.atStartOfDay(ZoneId.systemDefault()).toInstant()); + Date nextYearStart = Date.from(currentYearFirstDay.plusYears(1).atStartOfDay(ZoneId.systemDefault()).toInstant()); + cnd.and("startTime", ">=", currentYearStart); + cnd.and("startTime", "<", nextYearStart); cnd.desc("startTime"); Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); + fillAnsweredStatus(pagination); return Result.success(pagination); } + private void fillAnsweredStatus(Pagination pagination) { + List rows = pagination.getList(); + if (rows == null || rows.isEmpty()) { + return; + } + List activityIds = rows.stream() + .map(this::getRowActivityId) + .filter(id -> id != null && !id.isBlank()) + .collect(Collectors.toList()); + if (activityIds.isEmpty()) { + return; + } + Set answeredActivityIds = dao.query(QsvUserAnswerRecord.class, Cnd.where("userId", "=", SecurityUtil.getUserId()) + .and("activityId", "in", activityIds) + .and("isFinish", "=", true)) + .stream() + .map(QsvUserAnswerRecord::getActivityId) + .collect(Collectors.toSet()); + rows.forEach(row -> { + boolean isAnswered = answeredActivityIds.contains(getRowActivityId(row)); + row.put("isAnswered", isAnswered); + row.put("answeredText", isAnswered ? getAnsweredText(row.getString("category")) : ""); + }); + } + + private String getRowActivityId(NutMap row) { + String id = row.getString("id"); + if (id == null || id.isBlank()) { + id = row.getString("ID"); + } + if (id == null || id.isBlank()) { + id = row.getString("activityId"); + } + return id; + } + + private String getAnsweredText(String category) { + if ("QUIZ".equals(category)) { + return "已答题"; + } + if ("SURVEY".equals(category)) { + return "已填写"; + } + if ("VOTE".equals(category)) { + return "已投票"; + } + return "已完成"; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvQuizController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvQuizController.java index b2e1635d..fc49d076 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvQuizController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvQuizController.java @@ -1,7 +1,6 @@ package com.budwk.app.zhgh.dayofficework.qsv.h5controller; import cn.dev33.satoken.annotation.SaCheckLogin; -import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; @@ -9,7 +8,7 @@ import cn.hutool.json.JSONObject; import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.result.Result; import com.budwk.app.web.commons.auth.utils.SecurityUtil; -import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject; @@ -44,8 +43,6 @@ public class H5QsvQuizController { @Inject private Dao dao; @Inject - private ActivityBasicScopeService activityBasicScopeService; - @Inject private QsvUserAnswerRecordService qsvUserAnswerRecordService; @Inject private QsvQuizService qsvQuizService; @@ -71,7 +68,9 @@ public class H5QsvQuizController { QsvActivity activity = dao.fetch(QsvActivity.class, activityId); if (activity.getGroupId() != null) { - if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) { + int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId()) + .and("userId", "=", SecurityUtil.getUserId())); + if (count != 1) { return Result.error("您无需参加此次答题,感谢您的关注!"); } } @@ -103,7 +102,7 @@ public class H5QsvQuizController { List subjectIds = lastRecordOptional.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); answerRecordId = lastRecordOptional.get().getId(); } else { @@ -115,7 +114,7 @@ public class H5QsvQuizController { List subjectIds = answerRecord.getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); } } else { @@ -125,7 +124,9 @@ public class H5QsvQuizController { //首次进来生成答题记录 if ("ALL".equals(displayMode)) { resultSubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); - answerRecordId = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())).getId(); + QsvUserAnswerRecord answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, resultSubjects.stream().map(QsvSubject::getId).collect(Collectors.toList())); + answerRecordId = answerRecord.getId(); + resultSubjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds()); } else if ("RANDOM".equals(displayMode)) { //单次随机抽取题目数量 Integer randomCount = activity.getRandomCount(); @@ -144,7 +145,7 @@ public class H5QsvQuizController { List subjectIds = answerRecord.getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); } } else { @@ -170,7 +171,7 @@ public class H5QsvQuizController { } Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); repeatTips = true; } else { @@ -180,7 +181,7 @@ public class H5QsvQuizController { List subjectIds = maxAnswerRecordOptional.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); answerRecordId = maxAnswerRecordOptional.get().getId(); } else { @@ -193,7 +194,7 @@ public class H5QsvQuizController { List subjectIds = maxAnswerRecordOptional.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); answerRecordId = maxAnswerRecordOptional.get().getId(); } else { @@ -207,7 +208,7 @@ public class H5QsvQuizController { List subjectIds = notFinishRecordOptional.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); answerRecordId = notFinishRecordOptional.get().getId(); } else { @@ -245,7 +246,7 @@ public class H5QsvQuizController { List subjectIds = maxAnswerRecordOptional.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); answerRecordId = maxAnswerRecordOptional.get().getId(); } else { @@ -281,7 +282,7 @@ public class H5QsvQuizController { List subjectIds = answerRecord.getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); } else { //今天最大那次的记录 @@ -292,7 +293,7 @@ public class H5QsvQuizController { List subjectIds = maxTodayRecord.get().getSubjectIds(); Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); } else { throw new RuntimeException("业务异常"); @@ -306,12 +307,9 @@ public class H5QsvQuizController { answerRecordId = answerRecord.getId(); List subjectIds = answerRecord.getSubjectIds(); - if(CollectionUtil.isEmpty(subjectIds)){ - throw new RuntimeException("题目列表为空!请检查答题显示日期"); - } Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); resultSubjects = dao.query(QsvSubject.class, cnd); } } @@ -344,13 +342,6 @@ public class H5QsvQuizController { float totalScore = 0; for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) { - QsvSubject dbSubject = dao.fetch(QsvSubject.class, subject.getId()); - if (ObjectUtil.isNotEmpty(dbSubject) && "checkbox".equals(dbSubject.getType()) - && ObjectUtil.isNotEmpty(dbSubject.getMaxMulti()) && dbSubject.getMaxMulti() > 0 - && CollectionUtil.size(subject.getUserSelectOptionIds()) > dbSubject.getMaxMulti()) { - // 后端兜底校验最大可选数,避免绕过前端直接提交超限答案。 - return Result.error("题目【" + dbSubject.getTitle() + "】最多只能选择" + dbSubject.getMaxMulti() + "项"); - } JSONObject entries = extJson.get(subject.getId(), JSONObject.class); if (ObjectUtil.isNotEmpty(entries)) { entries.set("optionIds", subject.getUserSelectOptionIds()); @@ -395,7 +386,7 @@ public class H5QsvQuizController { Cnd cnd = Cnd.where("id", "in", subjectIds); String dynamic = subjectIds.stream().map(id -> "'" + id + "'").collect(Collectors.joining(",")); - cnd.orderBy("sortNum","asc"); + cnd.and(new Static("1=1 ORDER BY FIELD (id," + dynamic + ")")); List subjects = dao.query(QsvSubject.class, cnd); dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); @@ -407,4 +398,17 @@ public class H5QsvQuizController { return Result.success(result); } + private List querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(QsvSubject::getId, java.util.function.Function.identity(), (oldValue, newValue) -> oldValue)); + return subjectIds.stream() + .map(subjectMap::get) + .filter(ObjectUtil::isNotEmpty) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java index e784fa55..264af86d 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvSurveyController.java @@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONObject; import com.budwk.app.base.result.Result; import com.budwk.app.web.commons.auth.utils.SecurityUtil; -import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; @@ -12,6 +12,8 @@ import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; @@ -22,7 +24,11 @@ import org.nutz.mvc.annotation.Param; import javax.validation.Valid; import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; @IocBean @At("/platform/h5/qsv/survey") @@ -32,8 +38,6 @@ public class H5QsvSurveyController { @Inject private Dao dao; @Inject - private ActivityBasicScopeService activityBasicScopeService; - @Inject private QsvUserAnswerRecordService qsvUserAnswerRecordService; @At("") @@ -42,29 +46,37 @@ public class H5QsvSurveyController { } @At + @Aop(TransAop.READ_COMMITTED) public Result subjects(String activityId) { QsvActivity activity = dao.fetch(QsvActivity.class, activityId); + Result activityStatusResult = checkActivityStatus(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } - if (!activityBasicScopeService.isUserInGroup(activity.getGroupId(), SecurityUtil.getUserId())) { + int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId()) + .and("userId", "=", SecurityUtil.getUserId())); + if (count != 1) { return Result.error("您无需参加此次投票,感谢您的关注!"); } String answerRecordId = null; + List activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) .and("userId", "=", SecurityUtil.getUserId())); if (answerRecord == null) { - List subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); - qsvUserAnswerRecordService.insertRecord(activityId, subjects.stream().map(QsvSubject::getId).toList()); + qsvUserAnswerRecordService.insertRecord(activityId, activitySubjects.stream().map(QsvSubject::getId).toList()); } QsvUserAnswerRecord answerRecord2 = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) .and("userId", "=", SecurityUtil.getUserId())); + syncAnswerRecordSubjects(answerRecord2, activitySubjects); answerRecordId = answerRecord2.getId(); - List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()).asc("sortNum")); + List subjects = querySubjectsByRecordOrder(answerRecord2.getSubjectIds()); dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); for (QsvSubject subject : subjects) { @@ -77,6 +89,95 @@ public class H5QsvSurveyController { return Result.success(result); } + /** + * 校验调查活动是否处于可答题状态。activity 为空表示活动不存在; + * enabled 为 false 表示活动已关闭,当前时间早于 startTime 表示未开始,当前时间晚于 endTime 表示已结束。 + * + * @param activity 当前调查活动,包含开启状态、开始时间和结束时间 + * @return 状态或时间不允许答题时返回错误结果,允许答题时返回 null + */ + private Result checkActivityStatus(QsvActivity activity) { + if (activity == null) { + return Result.error("调查不存在"); + } + if (Boolean.FALSE.equals(activity.getEnabled())) { + return Result.error("活动已关闭,暂不能参与"); + } + Date now = new Date(); + if (activity.getStartTime() != null && activity.getStartTime().after(now)) { + return Result.error("调查尚未开始,请在开始后再参与"); + } + if (activity.getEndTime() != null && activity.getEndTime().before(now)) { + return Result.error("调查已结束"); + } + return null; + } + + /** + * 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID, + * 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。 + * + * @param answerRecord 用户当前问卷答题记录 + * @param activitySubjects 当前活动下按排序查询出的最新题目列表 + */ + private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List activitySubjects) { + if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return; + } + + List currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList()); + List recordSubjectIds = answerRecord.getSubjectIds(); + if (recordSubjectIds == null) { + recordSubjectIds = new ArrayList<>(); + } + + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + + LinkedHashSet currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds); + List normalizedSubjectIds = recordSubjectIds.stream() + .filter(currentSubjectIdSet::contains) + .collect(Collectors.toList()); + for (String subjectId : currentSubjectIds) { + if (!normalizedSubjectIds.contains(subjectId)) { + normalizedSubjectIds.add(subjectId); + } + } + + boolean changed = !recordSubjectIds.equals(normalizedSubjectIds); + for (String subjectId : currentSubjectIds) { + if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) { + JSONObject entry = new JSONObject(); + entry.set("optionIds", new ArrayList<>()); + entry.set("text", null); + entry.set("optionFillContents", new JSONObject()); + extJson.set(subjectId, entry); + changed = true; + } + } + + if (changed) { + answerRecord.setSubjectIds(normalizedSubjectIds); + answerRecord.setExtJson(extJson); + dao.update(answerRecord); + } + } + + private List querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue)); + return subjectIds.stream() + .map(subjectMap::get) + .filter(ObjectUtil::isNotEmpty) + .collect(Collectors.toList()); + } + @At public Result answerRecord(@Valid String answerRecordId) { QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId); @@ -84,7 +185,14 @@ public class H5QsvSurveyController { } @At + @Aop(TransAop.READ_COMMITTED) public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) { + QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId()); + Result activityStatusResult = checkActivityStatus(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } + QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId()); JSONObject extJson = answerRecord.getExtJson(); @@ -93,6 +201,7 @@ public class H5QsvSurveyController { if (ObjectUtil.isNotEmpty(entries)) { entries.set("optionIds", subject.getUserSelectOptionIds()); entries.set("text", subject.getUserFillContent()); + entries.set("optionFillContents", subject.getOptionFillContents()); } extJson.set(subject.getId(), entries); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java new file mode 100644 index 00000000..d1498afc --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/h5controller/H5QsvVoteController.java @@ -0,0 +1,301 @@ +package com.budwk.app.zhgh.dayofficework.qsv.h5controller; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONObject; +import com.budwk.app.base.result.Result; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; +import com.budwk.app.zhgh.dayofficework.qsv.param.QsvAnswerParam; +import com.budwk.app.zhgh.dayofficework.qsv.service.QsvUserAnswerRecordService; +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.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.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@IocBean +@At("/platform/h5/qsv/vote") +@Ok("json:full") +public class H5QsvVoteController { + + @Inject + private Dao dao; + @Inject + private QsvUserAnswerRecordService qsvUserAnswerRecordService; + + @At("") + @Ok("beetl:/platform/zhghh5/dayofficework/qsv/vote/index.html") + public void index() { + } + + @At + @Aop(TransAop.READ_COMMITTED) + public Result subjects(String activityId) { + QsvActivity activity = dao.fetch(QsvActivity.class, activityId); + Result activityStatusResult = checkActivityReadable(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } + + Result scopeResult = checkScope(activity); + if (scopeResult != null) { + return scopeResult; + } + + List activitySubjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); + QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("userId", "=", SecurityUtil.getUserId())); + if (answerRecord == null) { + List subjectIds = activitySubjects.stream() + .map(QsvSubject::getId) + .collect(Collectors.toList()); + answerRecord = qsvUserAnswerRecordService.insertRecord(activityId, subjectIds); + } + syncAnswerRecordSubjects(answerRecord, activitySubjects); + + List subjects = querySubjectsByRecordOrder(answerRecord.getSubjectIds()); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + for (QsvSubject subject : subjects) { + if (subject.getUserSelectOptionIds() == null) { + subject.setUserSelectOptionIds(new ArrayList<>()); + } + } + + NutMap result = NutMap.NEW() + .addv("subjects", subjects) + .addv("answerRecordId", answerRecord.getId()) + .addv("answerRecord", answerRecord) + .addv("activity", activity) + .addv("voteStats", buildVoteStats(activityId)); + return Result.success(result); + } + + @At + public Result answerRecord(@Valid String answerRecordId) { + QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId); + return Result.success(record); + } + + @At + public Result rank(@Valid String activityId) { + return Result.success(buildVoteStats(activityId)); + } + + @At + @Aop(TransAop.READ_COMMITTED) + public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) { + QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId()); + Result activityStatusResult = checkActivityWritable(activity); + if (activityStatusResult != null) { + return activityStatusResult; + } + + QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId()); + if (answerRecord == null || !SecurityUtil.getUserId().equals(answerRecord.getUserId())) { + return Result.error("\u6295\u7968\u8bb0\u5f55\u4e0d\u5b58\u5728"); + } + if (Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return Result.error("\u60a8\u5df2\u5b8c\u6210\u6295\u7968"); + } + + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + + for (QsvAnswerParam.Subject subject : qsvAnswerParam.getSubjects()) { + JSONObject entries = extJson.get(subject.getId(), JSONObject.class); + if (entries == null) { + entries = new JSONObject(); + } + entries.set("optionIds", subject.getUserSelectOptionIds()); + entries.set("text", subject.getUserFillContent()); + entries.set("optionFillContents", subject.getOptionFillContents()); + extJson.set(subject.getId(), entries); + } + + answerRecord.setExtJson(extJson); + answerRecord.setAttemptDate(new Date()); + answerRecord.setSubmitTime(new Date()); + answerRecord.setIsFinish(true); + dao.update(answerRecord); + + return Result.success("\u6295\u7968\u6210\u529f"); + } + + private Result checkActivityReadable(QsvActivity activity) { + if (activity == null) { + return Result.error("\u6295\u7968\u4e0d\u5b58\u5728"); + } + if (Boolean.FALSE.equals(activity.getEnabled())) { + return Result.error("\u6d3b\u52a8\u5df2\u5173\u95ed\uff0c\u6682\u4e0d\u80fd\u53c2\u4e0e"); + } + Date now = new Date(); + if (activity.getStartTime() != null && activity.getStartTime().after(now)) { + return Result.error("\u6295\u7968\u5c1a\u672a\u5f00\u59cb\uff0c\u8bf7\u5728\u5f00\u59cb\u540e\u518d\u53c2\u4e0e"); + } + return null; + } + + private Result checkActivityWritable(QsvActivity activity) { + Result readableResult = checkActivityReadable(activity); + if (readableResult != null) { + return readableResult; + } + Date now = new Date(); + if (activity.getEndTime() != null && activity.getEndTime().before(now)) { + return Result.error("\u6295\u7968\u5df2\u7ed3\u675f"); + } + return null; + } + + private Result checkScope(QsvActivity activity) { + if (activity.getGroupId() == null) { + return null; + } + int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId()) + .and("userId", "=", SecurityUtil.getUserId())); + if (count != 1) { + return Result.error("\u60a8\u65e0\u9700\u53c2\u52a0\u6b64\u6b21\u6295\u7968\uff0c\u611f\u8c22\u60a8\u7684\u5173\u6ce8\uff01"); + } + return null; + } + + private void syncAnswerRecordSubjects(QsvUserAnswerRecord answerRecord, List activitySubjects) { + if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) { + return; + } + List currentSubjectIds = activitySubjects.stream() + .map(QsvSubject::getId) + .collect(Collectors.toList()); + List recordSubjectIds = answerRecord.getSubjectIds(); + if (recordSubjectIds == null) { + recordSubjectIds = new ArrayList<>(); + } + JSONObject extJson = answerRecord.getExtJson(); + if (extJson == null) { + extJson = new JSONObject(); + } + + LinkedHashSet currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds); + List normalizedSubjectIds = recordSubjectIds.stream() + .filter(currentSubjectIdSet::contains) + .collect(Collectors.toList()); + for (String subjectId : currentSubjectIds) { + if (!normalizedSubjectIds.contains(subjectId)) { + normalizedSubjectIds.add(subjectId); + } + } + + boolean changed = !recordSubjectIds.equals(normalizedSubjectIds); + for (String subjectId : currentSubjectIds) { + if (ObjectUtil.isEmpty(extJson.get(subjectId, JSONObject.class))) { + JSONObject entry = new JSONObject(); + entry.set("optionIds", new ArrayList<>()); + entry.set("text", null); + entry.set("optionFillContents", new JSONObject()); + extJson.set(subjectId, entry); + changed = true; + } + } + + if (changed) { + answerRecord.setSubjectIds(normalizedSubjectIds); + answerRecord.setExtJson(extJson); + dao.update(answerRecord); + } + } + + private List querySubjectsByRecordOrder(List subjectIds) { + if (subjectIds == null || subjectIds.isEmpty()) { + return new ArrayList<>(); + } + List subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(QsvSubject::getId, Function.identity(), (oldValue, newValue) -> oldValue)); + return subjectIds.stream() + .map(subjectMap::get) + .filter(ObjectUtil::isNotEmpty) + .collect(Collectors.toList()); + } + + private NutMap buildVoteStats(String activityId) { + List subjects = dao.query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); + dao.fetchLinks(subjects, "options", Cnd.NEW().asc("sortNum")); + + Map optionStats = new LinkedHashMap<>(); + for (QsvSubject subject : subjects) { + if (subject.getOptions() == null) { + continue; + } + for (QsvOption option : subject.getOptions()) { + optionStats.put(option.getId(), NutMap.NEW() + .addv("optionId", option.getId()) + .addv("subjectId", subject.getId()) + .addv("text", option.getText()) + .addv("imgUrl", option.getImgUrl()) + .addv("votes", 0) + .addv("percent", 0D)); + } + } + + List records = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + for (QsvUserAnswerRecord record : records) { + JSONObject extJson = record.getExtJson(); + if (extJson == null) { + continue; + } + for (QsvSubject subject : subjects) { + JSONObject entry = extJson.get(subject.getId(), JSONObject.class); + if (entry == null) { + continue; + } + List optionIds = entry.getBeanList("optionIds", String.class); + if (optionIds == null) { + continue; + } + for (String optionId : optionIds) { + NutMap stat = optionStats.get(optionId); + if (stat != null) { + stat.put("votes", stat.getInt("votes", 0) + 1); + } + } + } + } + + int totalParticipants = records.size(); + List rankList = new ArrayList<>(optionStats.values()); + for (NutMap stat : rankList) { + int votes = stat.getInt("votes", 0); + double percent = totalParticipants == 0 ? 0D : votes * 100D / totalParticipants; + stat.put("percent", percent); + } + rankList.sort((left, right) -> Integer.compare(right.getInt("votes", 0), left.getInt("votes", 0))); + + return NutMap.NEW() + .addv("totalParticipants", totalParticipants) + .addv("optionCount", optionStats.size()) + .addv("optionStats", optionStats) + .addv("rankList", rankList); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvActivity.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvActivity.java index 35541473..ea94a8d9 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvActivity.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvActivity.java @@ -1,6 +1,8 @@ package com.budwk.app.zhgh.dayofficework.qsv.models; import com.budwk.app.base.model.BaseModel; +import com.budwk.app.sys.models.Sys_home_activity; +import com.budwk.app.sys.services.SysHomeConvert; import lombok.Data; import lombok.EqualsAndHashCode; import org.nutz.dao.entity.annotation.*; @@ -14,7 +16,7 @@ import java.util.Date; @Table("qsv_activity") @TableMeta("{'mysql-charset':'utf8mb4'}") @Comment("问卷调查投票活动表") -public class QsvActivity extends BaseModel implements Serializable { +public class QsvActivity extends BaseModel implements Serializable, SysHomeConvert { @Name @Comment("ID") @ColDefine(type = ColType.VARCHAR, width = 32) @@ -46,6 +48,12 @@ public class QsvActivity extends BaseModel implements Serializable { @ColDefine(type = ColType.DATETIME) private Date endTime; + @Column + @Comment("是否开启") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; + @Column @Comment("活动分组ID") @ColDefine(type = ColType.INT) @@ -107,4 +115,35 @@ public class QsvActivity extends BaseModel implements Serializable { @Comment("封面图片") @ColDefine(type = ColType.VARCHAR, width = 255) private String cover; + + @Override + public Sys_home_activity covertToSysHomeActivity() { + Sys_home_activity sysHomeActivity = new Sys_home_activity(); + sysHomeActivity.setId(this.getId()); + sysHomeActivity.setName(this.getTitle()); + sysHomeActivity.setCover(this.getCover()); + sysHomeActivity.setContent(this.getDescription()); + sysHomeActivity.setUrl("/platform/qsv/activity"); + sysHomeActivity.setH5Url(getH5Url()); + sysHomeActivity.setStartDate(this.getStartTime()); + sysHomeActivity.setEndDate(this.getEndTime()); + sysHomeActivity.setAllowUserGroupId(this.getGroupId()); + sysHomeActivity.setEnable(!Boolean.FALSE.equals(this.getEnabled())); + sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName()); + return sysHomeActivity; + } + + /** + * 根据活动类型生成移动端首页跳转地址。 + * QUIZ 跳转答题页,SURVEY 跳转调查页,VOTE 预留投票页地址,返回值为移动端路由字符串。 + */ + private String getH5Url() { + if ("SURVEY".equals(this.getCategory())) { + return "/platform/h5/qsv/survey?id=" + this.getId(); + } + if ("VOTE".equals(this.getCategory())) { + return "/platform/h5/qsv/vote?id=" + this.getId(); + } + return "/platform/h5/qsv/quiz?id=" + this.getId(); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java index 9c693558..7d53ae41 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvOption.java @@ -51,6 +51,16 @@ public class QsvOption extends BaseModel implements Serializable { @ColDefine(customType = "longtext") private String description; + @Column + @Comment("选中后是否需要填写补充内容") + @ColDefine(type = ColType.BOOLEAN) + private Boolean fillRequired; + + @Column + @Comment("补充内容输入提示") + @ColDefine(type = ColType.VARCHAR, width = 255) + private String fillPlaceholder; + @Column @Comment("排序") @ColDefine(type = ColType.INT) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java index b72f5bc6..47e6d63d 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/models/QsvSubject.java @@ -58,11 +58,41 @@ public class QsvSubject extends BaseModel implements Serializable { @ColDefine(type = ColType.INT, width = 1) private Integer maxMulti; + @Column + @Comment("投票选项排列方式") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String optionLayout; + + @Column + @Comment("投票选项横向列数") + @ColDefine(type = ColType.INT, width = 1) + private Integer optionColumns; + @Column @Comment("排序") @ColDefine(type = ColType.INT, width = 1) private Integer sortNum; + @Column + @Comment("是否启用题目隐显逻辑") + @ColDefine(type = ColType.BOOLEAN) + private Boolean visibleRuleEnabled; + + @Column + @Comment("隐显逻辑处理方式(show显示,hide隐藏)") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String visibleRuleAction; + + @Column + @Comment("隐显逻辑条件关系(AND并且,OR或者)") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String visibleRuleLogic; + + @Column + @Comment("隐显逻辑条件列表") + @ColDefine(type = ColType.MYSQL_JSON) + private List visibleRuleConditions; + /** * 选项 */ @@ -79,4 +109,52 @@ public class QsvSubject extends BaseModel implements Serializable { */ private String userFillContent; + /** + * 题目隐显逻辑条件,一条条件表示选择指定题目的指定选项后参与显示或隐藏判断。 + */ + @Data + public static class VisibleRuleCondition implements Serializable { + + /** + * 作为触发条件的题目ID。 + */ + private String subjectId; + + /** + * 作为触发条件的选项ID。 + */ + private String optionId; + + /** + * 条件节点类型:group 分组,item 条件项。 + */ + private String type; + + /** + * 分组内条件关系:and 并且,or 或者。 + */ + private String logic; + + /** + * 条件项引用的题目ID。 + */ + private String field; + + /** + * 比较符:==、!=、contains、>、<、>=、<=。 + */ + private String operator; + + /** + * 比较值,选择题为选项ID,填空题可为文本或数字。 + */ + private Object value; + + /** + * 子条件节点,支持条件分组嵌套。 + */ + private List children; + + } + } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java index c614794a..6c7e2e37 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/param/QsvAnswerParam.java @@ -3,6 +3,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.param; import lombok.Data; import java.util.List; +import java.util.Map; @Data //答题参数 @@ -21,6 +22,7 @@ public class QsvAnswerParam { private String id; private List userSelectOptionIds; private String userFillContent; + private Map optionFillContents; } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvActivityService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvActivityService.java index 27278a2b..f57b8806 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvActivityService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvActivityService.java @@ -5,4 +5,10 @@ import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; public interface QsvActivityService extends BaseService { + + QsvActivity saveActivity(QsvActivity qsvActivity); + + void deleteActivity(String id); + + void updateEnabled(String id, Boolean enabled); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvSurveyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvSurveyService.java index eca20ddc..00e9a464 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvSurveyService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/QsvSurveyService.java @@ -12,6 +12,14 @@ public interface QsvSurveyService extends BaseService { List report(String activityId); + List categoryReport(String activityId, String conditions); + + NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions); + + NutMap compareReport(String activityId, String subjectId, String optionId); + + List selectOptionUsers(String activityId, String subjectId, String optionId, String conditions); + void exportReportXlsx(String activityId, HttpServletResponse response); void exportUserAnswerXlsx(String activityId, HttpServletResponse response); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvActivityServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvActivityServiceImpl.java index 1d77d6b7..94f01028 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvActivityServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvActivityServiceImpl.java @@ -1,15 +1,82 @@ package com.budwk.app.zhgh.dayofficework.qsv.service.impl; import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.sys.models.Sys_home_activity; import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvOption; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject; +import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord; import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService; +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.IocBean; +import java.util.List; +import java.util.stream.Collectors; + @IocBean(args = {"refer:dao"}) public class QsvActivityServiceImpl extends BaseServiceImpl implements QsvActivityService { public QsvActivityServiceImpl(Dao dao) { super(dao); } + + @Override + @Aop(TransAop.READ_COMMITTED) + public QsvActivity saveActivity(QsvActivity qsvActivity) { + fillQuizRepeatMode(qsvActivity); + dao().insertOrUpdate(qsvActivity); + syncHomeActivity(qsvActivity); + return qsvActivity; + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void deleteActivity(String id) { + List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", id)); + List subjectIds = subjects.stream().map(QsvSubject::getId).collect(Collectors.toList()); + if (!subjectIds.isEmpty()) { + dao().clear(QsvOption.class, Cnd.where("subjectId", "in", subjectIds)); + dao().clear(QsvSubject.class, Cnd.where("id", "in", subjectIds)); + } + dao().clear(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", id)); + dao().delete(QsvActivity.class, id); + dao().delete(Sys_home_activity.class, id); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void updateEnabled(String id, Boolean enabled) { + QsvActivity activity = dao().fetch(QsvActivity.class, id); + if (activity == null) { + return; + } + activity.setEnabled(enabled); + dao().update(activity); + syncHomeActivity(activity); + } + + /** + * 答题活动的重复模式由出题模式决定。定时定题按天统计,常规模式按活动总次数统计。 + */ + private void fillQuizRepeatMode(QsvActivity qsvActivity) { + if (!"QUIZ".equals(qsvActivity.getCategory())) { + return; + } + if ("SCHEDULED".equals(qsvActivity.getMode())) { + qsvActivity.setRepeatMode("DAILY"); + } else if ("REGULAR".equals(qsvActivity.getMode())) { + qsvActivity.setRepeatMode("TOTAL"); + } + } + + /** + * 同步移动端首页活动。问卷活动保存或启停后,都以活动自身 enabled、时间、人员范围生成首页记录。 + */ + private void syncHomeActivity(QsvActivity activity) { + Sys_home_activity sysHomeActivity = activity.covertToSysHomeActivity(); + dao().insertOrUpdate(sysHomeActivity); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvQuizRankServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvQuizRankServiceImpl.java index 3cc084a3..9e64b2a4 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvQuizRankServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvQuizRankServiceImpl.java @@ -119,17 +119,11 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl 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 ("SCHEDULED".equals(mode) || "REGULAR".equals(mode)) { + if ("HIGH".equals(scoreStatisticsMode) || "HIGHEST".equals(scoreStatisticsMode)) { + cnd.and("t1.isHighestScore", "=", 1); + } else { + cnd.and("t1.isLatestScore", "=", 1); } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java index ff21f115..231be8a3 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvSurveyServiceImpl.java @@ -8,6 +8,7 @@ import cn.afterturn.easypoi.excel.export.ExcelExportService; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.service.impl.BaseServiceImpl; @@ -68,47 +69,47 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // 处理每个题目 for (NutMap subject : subjects) { // 获取当前题目的选项列表 - List subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class); + List currentOptions = optionsGroup.get(subject.getString("id")); + List subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class); // 获取题目类型 String subjectType = subject.getString("type"); // 处理文本类型题目 if ("text".equals(subjectType)) { - List 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")) + List textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id")); + List texts = textAnswers.stream() + .map(textAnswer -> textAnswer.getString("text")) .toList(); subject.put("texts", texts); + subject.put("textAnswers", textAnswers); } // 处理单选类型题目 处理多选类型题目 else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) { long selectTotal = answerExtList.stream() - .filter(ext -> ext.containsKey(subject.getString("id"))) + .filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id"))) .count(); subjectOptions.forEach(subjectOption -> { long selectCount = answerExtList.stream() .filter(ext -> { + if (ObjectUtil.isEmpty(ext)) { + return false; + } JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class); return jsonObject != null && jsonObject.getJSONArray("optionIds") != null && jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id")); }) .count(); - long round = Math.round((double) selectCount / selectTotal * 100); + long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100); subjectOption.put("selectPercent", round + "%"); subjectOption.put("selectCount", selectCount); }); subjectOptions.sort((o1, o2) -> { - int count1 = o1.getInt("selectCount"); - int count2 = o2.getInt("selectCount"); - if (count1 == count2) { - return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")); - } - return Integer.compare(count2, count1); + return Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum")); }); subject.put("selectTotal", selectTotal); @@ -125,6 +126,228 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i } + @Override + public List categoryReport(String activityId, String conditions) { + if (activityId == null || activityId.isEmpty()) { + throw new IllegalArgumentException("activityId不能为空"); + } + try { + List conditionList = parseCategoryConditions(conditions); + List answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + return buildReport(activityId, filterAnswerRecords(answerRecords, conditionList)); + } catch (Exception e) { + log.error("分类报告生成失败", e); + throw new RuntimeException("分类报告生成失败", e); + } + } + + @Override + public NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions) { + if (activityId == null || activityId.isEmpty()) { + throw new IllegalArgumentException("activityId不能为空"); + } + List xSubjectIdList = parseSubjectIds(xSubjectIds); + List ySubjectIdList = parseSubjectIds(ySubjectIds); + if (ObjectUtil.isEmpty(xSubjectIdList) || ObjectUtil.isEmpty(ySubjectIdList)) { + throw new IllegalArgumentException("请选择自变量X和因变量Y"); + } + if (xSubjectIdList.stream().anyMatch(ySubjectIdList::contains)) { + throw new IllegalArgumentException("自变量X和因变量Y不能选择相同变量"); + } + + try { + List subjects = querySubjects(activityId); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject)); + if (!subjectMap.keySet().containsAll(xSubjectIdList) || !subjectMap.keySet().containsAll(ySubjectIdList)) { + throw new IllegalArgumentException("题目不存在"); + } + + List allSubjectIds = new ArrayList<>(); + allSubjectIds.addAll(xSubjectIdList); + allSubjectIds.addAll(ySubjectIdList); + List options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", allSubjectIds).asc("sortNum")); + Map> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId)); + List xOptionMaps = buildVariableOptionCombinations(xSubjectIdList, subjectMap, optionGroup); + List conditionList = parseCategoryConditions(conditions); + List answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + List filteredRecords = filterAnswerRecords(answerRecords, conditionList); + List reports = new ArrayList<>(); + + for (String ySubjectId : ySubjectIdList) { + NutMap ySubject = subjectMap.get(ySubjectId); + List yOptions = optionGroup.get(ySubjectId); + if (ObjectUtil.isEmpty(yOptions)) { + yOptions = new ArrayList<>(); + } + List yOptionMaps = ObjectUtil.isEmpty(yOptions) ? new ArrayList<>() : Lang.collection2list(yOptions, NutMap.class); + List rows = new ArrayList<>(); + + for (NutMap xOption : xOptionMaps) { + List xConditions = xOption.getList("conditions", NutMap.class); + List xMatchedRecords = filteredRecords.stream() + .filter(record -> isRecordMatched(record, xConditions)) + .toList(); + long rowTotal = xMatchedRecords.size(); + List cells = new ArrayList<>(); + for (QsvOption yOption : yOptions) { + List yCondition = List.of(NutMap.NEW() + .addv("subjectId", ySubjectId) + .addv("optionId", yOption.getId())); + long count = xMatchedRecords.stream() + .filter(record -> isRecordMatched(record, yCondition)) + .count(); + double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0; + cells.add(NutMap.NEW() + .addv("yOptionId", yOption.getId()) + .addv("yOptionText", yOption.getText()) + .addv("count", count) + .addv("percent", percent)); + } + rows.add(NutMap.NEW() + .addv("xOptionId", xOption.getString("id")) + .addv("xOptionText", xOption.getString("text")) + .addv("cells", cells) + .addv("total", rowTotal)); + } + + reports.add(NutMap.NEW() + .addv("ySubject", ySubject) + .addv("yOptions", yOptionMaps) + .addv("rows", rows)); + } + + return NutMap.NEW() + .addv("xSubject", buildVariableSubject(xSubjectIdList, subjectMap)) + .addv("xOptions", xOptionMaps) + .addv("reports", reports) + .addv("total", filteredRecords.size()); + } catch (Exception e) { + log.error("交叉分析报告生成失败", e); + throw new RuntimeException("交叉分析报告生成失败", e); + } + } + + @Override + public NutMap compareReport(String activityId, String subjectId, String optionId) { + if (activityId == null || activityId.isEmpty()) { + throw new IllegalArgumentException("activityId不能为空"); + } + if (ObjectUtil.isEmpty(subjectId) || ObjectUtil.isEmpty(optionId)) { + throw new IllegalArgumentException("请选择对比变量和选项"); + } + + try { + List subjects = querySubjects(activityId); + Map subjectMap = subjects.stream() + .collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject)); + NutMap compareSubject = subjectMap.get(subjectId); + if (compareSubject == null) { + throw new IllegalArgumentException("对比变量不存在"); + } + + List subjectIds = subjects.stream().map(subject -> subject.getString("id")).toList(); + List options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum")); + Map> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId)); + QsvOption compareOption = optionGroup.getOrDefault(subjectId, new ArrayList<>()).stream() + .filter(option -> optionId.equals(option.getId())) + .findFirst() + .orElse(null); + if (compareOption == null) { + throw new IllegalArgumentException("对比选项不存在"); + } + + List compareCondition = List.of(NutMap.NEW() + .addv("subjectId", subjectId) + .addv("optionId", optionId)); + List answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + List filteredRecords = filterAnswerRecords(answerRecords, compareCondition); + List reports = new ArrayList<>(); + + for (NutMap subject : subjects) { + String currentSubjectId = subject.getString("id"); + String subjectType = subject.getString("type"); + if (subjectId.equals(currentSubjectId) || (!"radio".equals(subjectType) && !"checkbox".equals(subjectType))) { + continue; + } + List currentOptions = optionGroup.get(currentSubjectId); + if (ObjectUtil.isEmpty(currentOptions)) { + continue; + } + List cells = new ArrayList<>(); + long rowTotal = filteredRecords.size(); + for (QsvOption option : currentOptions) { + List optionCondition = List.of(NutMap.NEW() + .addv("subjectId", currentSubjectId) + .addv("optionId", option.getId())); + long count = filteredRecords.stream() + .filter(record -> isRecordMatched(record, optionCondition)) + .count(); + double percent = rowTotal == 0 ? 0 : Math.round((double) count / rowTotal * 10000) / 100.0; + cells.add(NutMap.NEW() + .addv("yOptionId", option.getId()) + .addv("yOptionText", option.getText()) + .addv("count", count) + .addv("percent", percent)); + } + reports.add(NutMap.NEW() + .addv("ySubject", subject) + .addv("yOptions", Lang.collection2list(currentOptions, NutMap.class)) + .addv("rows", List.of(NutMap.NEW() + .addv("xOptionId", optionId) + .addv("xOptionText", compareOption.getText()) + .addv("cells", cells) + .addv("total", rowTotal)))); + } + + return NutMap.NEW() + .addv("compareSubject", compareSubject) + .addv("compareOption", Lang.obj2nutmap(compareOption)) + .addv("reports", reports) + .addv("total", filteredRecords.size()); + } catch (Exception e) { + log.error("对比分析报告生成失败", e); + throw new RuntimeException("对比分析报告生成失败", e); + } + } + + @Override + public List selectOptionUsers(String activityId, String subjectId, String optionId, String conditions) { + List conditionList = parseCategoryConditions(conditions); + List answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId) + .and("isFinish", "=", true)); + return filterAnswerRecords(answerRecords, conditionList).stream().map(record -> { + JSONObject extJson = record.getExtJson(); + if (ObjectUtil.isEmpty(extJson)) { + return null; + } + JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class); + if (ObjectUtil.isEmpty(subjectAnswer)) { + return null; + } + JSONArray optionIds = subjectAnswer.getJSONArray("optionIds"); + if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(optionId)) { + return null; + } + JSONObject optionFillContents = subjectAnswer.getJSONObject("optionFillContents"); + String fillContent = ObjectUtil.isEmpty(optionFillContents) ? "" : optionFillContents.getStr(optionId); + return NutMap.NEW() + .addv("userName", record.getUserName()) + .addv("loginName", record.getLoginName()) + .addv("unionName", record.getUnionName()) + .addv("unitName", record.getUnitName()) + .addv("attemptDate", record.getAttemptDate()) + .addv("submitTime", record.getSubmitTime()) + .addv("fillContent", fillContent); + }) + .filter(ObjectUtil::isNotNull) + .toList(); + } + + @Override public void exportReportXlsx(String activityId, HttpServletResponse response) { List report = report(activityId); @@ -134,10 +357,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20)); exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20)); - Map> listMap = report.stream().collect(Collectors.groupingBy(v -> v.getString("id"))); Workbook workbook = new XSSFWorkbook(); - listMap.forEach((k, v) -> { - NutMap nutMap = v.get(0); + report.forEach(nutMap -> { ExcelExportService service = new ExcelExportService(); ExportParams exportParams = new ExportParams(); exportParams.setTitle(nutMap.getString("title")); @@ -171,7 +392,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i List answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList(); // 查询活动题目列表 - List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId)); + List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); // 将题目列表转换为Map Map subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v)); // 获取题目ID列表 @@ -185,7 +406,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // 构建Excel导出实体 List excelExportEntities = buildExcelExportEntities(subjects); // 构建答题记录列表 - List list = buildAnswerRecords(answerRecords, subjectMap, options); + List list = buildAnswerRecords(answerRecords, subjectMap, optionMap); // 设置导出参数 ExportParams exportParams = new ExportParams(); @@ -223,7 +444,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i // List answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList(); // 查询活动题目列表 - List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId)); + List subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum")); // 将题目列表转换为Map Map subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v)); // 获取题目ID列表 @@ -245,18 +466,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i .addv("unitName", record.getString("unitName")) .addv("unionName", record.getString("unionName")); JSONObject extJson = record.getAs("extJson", JSONObject.class); + if (ObjectUtil.isEmpty(extJson)) { + return map; + } 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); + QsvSubject subject = subjectMap.get(k); + if (subject == null || !(v instanceof JSONObject jsonVal)) { + return; } + map.addv(k, buildAnswerText(jsonVal, subject, optionMap)); }); return map; }).toList(); @@ -278,6 +496,171 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i } } + private List buildReport(String activityId, List answerRecords) { + List answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList(); + List subjects = querySubjects(activityId); + List subjectIds = subjects.stream().map(v -> v.getString("id")).toList(); + List options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum")); + Map> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId)); + + for (NutMap subject : subjects) { + List currentOptions = optionsGroup.get(subject.getString("id")); + List subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class); + String subjectType = subject.getString("type"); + if ("text".equals(subjectType)) { + List textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id")); + List texts = textAnswers.stream() + .map(textAnswer -> textAnswer.getString("text")) + .toList(); + subject.put("texts", texts); + subject.put("textAnswers", textAnswers); + } else if ("radio".equals(subjectType) || "checkbox".equals(subjectType)) { + long selectTotal = answerExtList.stream() + .filter(ext -> ObjectUtil.isNotEmpty(ext) && ext.containsKey(subject.getString("id"))) + .count(); + subjectOptions.forEach(subjectOption -> { + long selectCount = answerExtList.stream() + .filter(ext -> { + if (ObjectUtil.isEmpty(ext)) { + return false; + } + JSONObject jsonObject = ext.get(subject.getString("id"), JSONObject.class); + return jsonObject != null + && jsonObject.getJSONArray("optionIds") != null + && jsonObject.getJSONArray("optionIds").contains(subjectOption.getString("id")); + }) + .count(); + long round = selectTotal == 0 ? 0 : Math.round((double) selectCount / selectTotal * 100); + subjectOption.put("selectPercent", round + "%"); + subjectOption.put("selectCount", selectCount); + }); + subjectOptions.sort((o1, o2) -> Integer.compare(o1.getInt("sortNum"), o2.getInt("sortNum"))); + subject.put("selectTotal", selectTotal); + } + subject.addv("options", subjectOptions); + } + return subjects; + } + + private List parseCategoryConditions(String conditions) { + if (ObjectUtil.isEmpty(conditions)) { + return new ArrayList<>(); + } + return JSONUtil.parseArray(conditions).toList(NutMap.class).stream() + .filter(condition -> ObjectUtil.isNotEmpty(condition.getString("subjectId")) + && ObjectUtil.isNotEmpty(condition.getString("optionId"))) + .toList(); + } + + private List parseSubjectIds(String subjectIds) { + if (ObjectUtil.isEmpty(subjectIds)) { + return new ArrayList<>(); + } + String value = subjectIds.trim(); + List result = new ArrayList<>(); + if (value.startsWith("[")) { + for (Object subjectId : JSONUtil.parseArray(value)) { + if (ObjectUtil.isNotEmpty(subjectId)) { + result.add(String.valueOf(subjectId)); + } + } + return result.stream().distinct().toList(); + } + for (String subjectId : value.split(",")) { + if (ObjectUtil.isNotEmpty(subjectId)) { + result.add(subjectId.trim()); + } + } + return result.stream().distinct().toList(); + } + + private NutMap buildVariableSubject(List subjectIds, Map subjectMap) { + String title = subjectIds.stream() + .map(subjectId -> subjectMap.get(subjectId).getString("title")) + .collect(Collectors.joining(" / ")); + return NutMap.NEW() + .addv("id", String.join("|", subjectIds)) + .addv("title", title); + } + + private List buildVariableOptionCombinations(List subjectIds, Map subjectMap, Map> optionGroup) { + List> subjectOptionGroups = new ArrayList<>(); + for (String subjectId : subjectIds) { + NutMap subject = subjectMap.get(subjectId); + List options = optionGroup.get(subjectId); + if (ObjectUtil.isEmpty(options)) { + continue; + } + List optionMaps = options.stream() + .map(option -> NutMap.NEW() + .addv("subjectId", subjectId) + .addv("subjectTitle", subject.getString("title")) + .addv("optionId", option.getId()) + .addv("optionText", option.getText())) + .toList(); + subjectOptionGroups.add(optionMaps); + } + if (subjectOptionGroups.size() != subjectIds.size()) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + buildVariableOptionCombinations(subjectOptionGroups, 0, new ArrayList<>(), result); + return result; + } + + private void buildVariableOptionCombinations(List> subjectOptionGroups, int index, List current, List result) { + if (index >= subjectOptionGroups.size()) { + String id = current.stream() + .map(option -> option.getString("subjectId") + ":" + option.getString("optionId")) + .collect(Collectors.joining("|")); + String text = current.stream() + .map(option -> option.getString("optionText")) + .collect(Collectors.joining(" / ")); + List conditions = current.stream() + .map(option -> NutMap.NEW() + .addv("subjectId", option.getString("subjectId")) + .addv("optionId", option.getString("optionId"))) + .toList(); + result.add(NutMap.NEW() + .addv("id", id) + .addv("text", text) + .addv("conditions", conditions)); + return; + } + for (NutMap option : subjectOptionGroups.get(index)) { + current.add(option); + buildVariableOptionCombinations(subjectOptionGroups, index + 1, current, result); + current.remove(current.size() - 1); + } + } + + private List filterAnswerRecords(List answerRecords, List conditions) { + if (ObjectUtil.isEmpty(conditions)) { + return answerRecords; + } + return answerRecords.stream() + .filter(record -> isRecordMatched(record, conditions)) + .toList(); + } + + private boolean isRecordMatched(QsvUserAnswerRecord record, List conditions) { + JSONObject extJson = record.getExtJson(); + if (ObjectUtil.isEmpty(extJson)) { + return false; + } + for (NutMap condition : conditions) { + JSONObject subjectAnswer = extJson.get(condition.getString("subjectId"), JSONObject.class); + if (ObjectUtil.isEmpty(subjectAnswer)) { + return false; + } + JSONArray optionIds = subjectAnswer.getJSONArray("optionIds"); + if (ObjectUtil.isEmpty(optionIds) || !optionIds.contains(condition.getString("optionId"))) { + return false; + } + } + return true; + } + private List querySubjects(String activityId) { Sql subjectSql = Sqls.create(""" SELECT @@ -309,7 +692,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i return excelExportEntities; } - private List buildAnswerRecords(List answerRecords, Map subjectMap, List options) { + /** + * 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息, + * 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。 + * + * @param answerRecords 当前活动已完成的答题记录 + * @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容 + * @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text + */ + private List buildTextAnswerDetails(List answerRecords, String subjectId) { + List textAnswers = new ArrayList<>(); + for (QsvUserAnswerRecord answerRecord : answerRecords) { + JSONObject extJson = answerRecord.getExtJson(); + if (ObjectUtil.isEmpty(extJson)) { + continue; + } + JSONObject subjectAnswer = extJson.get(subjectId, JSONObject.class); + if (ObjectUtil.isEmpty(subjectAnswer) || ObjectUtil.isEmpty(subjectAnswer.getStr("text"))) { + continue; + } + textAnswers.add(NutMap.NEW() + .addv("loginName", answerRecord.getLoginName()) + .addv("userName", answerRecord.getUserName()) + .addv("unionName", answerRecord.getUnionName()) + .addv("unitName", answerRecord.getUnitName()) + .addv("submitTime", answerRecord.getSubmitTime()) + .addv("text", subjectAnswer.getStr("text"))); + } + return textAnswers; + } + + private List buildAnswerRecords(List answerRecords, Map subjectMap, Map optionMap) { return answerRecords.stream().map(record -> { NutMap map = NutMap.NEW() .addv("id", record.getId()) @@ -318,20 +731,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl i .addv("unitName", record.getUnitName()) .addv("unionName", record.getUnionName()); JSONObject extJson = record.getExtJson(); + if (ObjectUtil.isEmpty(extJson)) { + return map; + } 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); + QsvSubject subject = subjectMap.get(k); + if (subject == null || !(v instanceof JSONObject jsonVal)) { + return; } + map.addv(k, buildAnswerText(jsonVal, subject, optionMap)); }); return map; }).collect(Collectors.toList()); } + + /** + * 组装单题答案展示内容。填空题返回填写文本;选择题返回选项文本,若选项配置了补充填写, + * 则追加对应补充内容,保证后台查看答卷和导出能看到用户填写的说明。 + * + * @param jsonVal 答题记录 extJson 中当前题目的答案对象,包含 optionIds、text、optionFillContents + * @param subject 当前题目,用于判断题目类型 + * @param optionMap 当前活动所有选项,key 为选项ID,value 为选项实体 + * @return 当前题目的答案展示文本 + */ + private String buildAnswerText(JSONObject jsonVal, QsvSubject subject, Map optionMap) { + String type = subject.getType(); + if ("text".equals(type)) { + return jsonVal.getStr("text"); + } + + if (!"radio".equals(type) && !"checkbox".equals(type)) { + return ""; + } + + JSONArray optionIds = jsonVal.getJSONArray("optionIds"); + if (ObjectUtil.isEmpty(optionIds)) { + return ""; + } + + JSONObject optionFillContents = jsonVal.getJSONObject("optionFillContents"); + List answerTexts = new ArrayList<>(); + for (Object optionIdObj : optionIds) { + String optionId = String.valueOf(optionIdObj); + QsvOption option = optionMap.get(optionId); + if (option == null) { + continue; + } + String answerText = option.getText(); + if (ObjectUtil.isNotEmpty(optionFillContents)) { + String fillContent = optionFillContents.getStr(optionId); + if (ObjectUtil.isNotEmpty(fillContent)) { + answerText = answerText + ":" + fillContent; + } + } + answerTexts.add(answerText); + } + return String.join(";", answerTexts); + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvUserAnswerRecordServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvUserAnswerRecordServiceImpl.java index e22005ca..93aa6ea0 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvUserAnswerRecordServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/qsv/service/impl/QsvUserAnswerRecordServiceImpl.java @@ -121,11 +121,12 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl chairmen = memberInfoService.dao().query(Sys_union_cadre.class, + Cnd.where("unionId", "=", unionId) + .and("roleCode", "=", "BRANCH_UNION_CHAIRMAN") + .and(Cnd.exps("isServing", "=", true).or("isServing", "is", null))); + String chairman = chairmen.stream() + .map(item -> item.getUserName() + "(" + item.getLoginName() + ")") + .collect(Collectors.joining("、")); + String chairmanMobile = chairmen.stream() + .map(Sys_union_cadre::getMobile) + .filter(mobile -> mobile != null && !mobile.isBlank()) + .collect(Collectors.joining("、")); + + return Result.success(NutMap.NEW() + .addv("unionName", union.getName()) + .addv("unionCode", union.getUnionCode()) + .addv("chairman", chairman) + .addv("telephone", union.getTelephone()) + .addv("chairmanMobile", chairmanMobile) + .addv("memberTotal", memberTotal) + .addv("staffTotal", staffTotal) + .addv("maleTotal", maleTotal) + .addv("femaleTotal", femaleTotal)); + } + @At @SaCheckPermission("member.info.group") diff --git a/src/main/resources/db/menu/init_qsv_menu.sql b/src/main/resources/db/menu/init_qsv_menu.sql new file mode 100644 index 00000000..eda22599 --- /dev/null +++ b/src/main/resources/db/menu/init_qsv_menu.sql @@ -0,0 +1,160 @@ +-- 问卷服务菜单初始化 +-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。 + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT + 'f6c0c37d2d7a4a22b5c7b1a1qsv0001', + '', + LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'), + '日常办公', + 'Day Office Work', + 'menu', + '', + '', + 'ti-briefcase', + 1, + 0, + 'dayofficework', + NULL, + 600, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0, + 'PC', + NULL, + NULL, + 'r', + 0, + 0 +FROM sys_menu +WHERE (parentId = '' OR parentId IS NULL) + AND CHAR_LENGTH(path) = 4 +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'dayofficework') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT + 'f6c0c37d2d7a4a22b5c7b1a1qsv0002', + p.id, + CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), + '问卷服务', + 'Questionnaire Service', + 'menu', + '', + '', + 'fa fa-list-alt', + 1, + 0, + 'qsv', + NULL, + 1, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0, + 'PC', + p.moduleId, + NULL, + 'w', + 0, + 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'dayofficework' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0003', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '问卷管理', 'Questionnaire Manage', 'menu', '/platform/qsv/activity', 'data-pjax', '', 1, 0, 'qsv.activity', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'w', 0, 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'qsv' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.activity') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0004', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '题库管理', 'Question Bank', 'menu', '/platform/qsv/bank', 'data-pjax', '', 1, 0, 'qsv.bank', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 't', 0, 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'qsv' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.bank') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0005', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '调查统计', 'Survey Statistics', 'menu', '/platform/qsv/survey', 'data-pjax', '', 1, 0, 'qsv.survey', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'd', 0, 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'qsv' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.survey') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0006', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '答题排行', 'Quiz Rank', 'menu', '/platform/qsv/quizRank', 'data-pjax', '', 1, 0, 'qsv.quiz.rank', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'd', 0, 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'qsv' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.quiz.rank') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'f6c0c37d2d7a4a22b5c7b1a1qsv0007', p.id, CONCAT(p.path, LPAD(IFNULL(MAX(CAST(SUBSTRING(c.path, CHAR_LENGTH(p.path) + 1, 4) AS UNSIGNED)), 0) + 1, 4, '0')), '在线答题', 'Online Quiz', 'menu', '/platform/qsv/online', 'data-pjax', '', 1, 0, 'qsv.online', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', p.moduleId, NULL, 'z', 0, 0 +FROM sys_menu p +LEFT JOIN sys_menu c ON c.parentId = p.id AND CHAR_LENGTH(c.path) = CHAR_LENGTH(p.path) + 4 +WHERE p.permission = 'qsv' +GROUP BY p.id, p.path, p.moduleId +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'qsv.online') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT + 'f6c0c37d2d7a4a22b5c7b1a1qsv0008', + '', + LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'), + '问卷服务', + 'Questionnaire Service', + 'menu', + '/platform/h5/qsv', + 'data-pjax', + '', + 1, + 0, + 'h5.qsv', + NULL, + 601, + 0, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0, + 'H5', + NULL, + '/assets/mobile/svg/qsv/icon.svg', + 'w', + 1, + 1 +FROM sys_menu +WHERE (parentId = '' OR parentId IS NULL) + AND CHAR_LENGTH(path) = 4 +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'h5.qsv') t); + +-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。 +INSERT INTO sys_role_menu (roleId, menuId) +SELECT r.id, m.id +FROM sys_role r +JOIN sys_menu m ON m.permission IN ( + 'dayofficework', + 'qsv', + 'qsv.activity', + 'qsv.bank', + 'qsv.survey', + 'qsv.quiz.rank', + 'qsv.online', + 'h5.qsv' +) +LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id +WHERE r.code = 'SYSADMIN' + AND rm.roleId IS NULL; diff --git a/src/main/resources/db/qsv_subject_add_vote_option_layout.sql b/src/main/resources/db/qsv_subject_add_vote_option_layout.sql new file mode 100644 index 00000000..2b07d078 --- /dev/null +++ b/src/main/resources/db/qsv_subject_add_vote_option_layout.sql @@ -0,0 +1,3 @@ +ALTER TABLE qsv_subject + ADD COLUMN option_layout VARCHAR(20) DEFAULT 'VERTICAL' COMMENT '投票选项排列方式', + ADD COLUMN option_columns INT DEFAULT 1 COMMENT '投票选项横向列数'; diff --git a/src/main/resources/views/layouts/platform.html b/src/main/resources/views/layouts/platform.html index c6165e7c..3575c275 100644 --- a/src/main/resources/views/layouts/platform.html +++ b/src/main/resources/views/layouts/platform.html @@ -365,6 +365,7 @@ layout("/layouts/v4/baseLayout.html"){ // 地址选中 hrefSelect() { + const targetHref = this.getMenuOwnerPath() function findMenuPath(menus, targetHref) { function search(items, path) { for (const item of items) { @@ -383,7 +384,7 @@ layout("/layouts/v4/baseLayout.html"){ return search(menus, []); } - const result = findMenuPath(this.menus, window.location.pathname) + const result = findMenuPath(this.menus, targetHref) if(result){ this.activeMenuIndex = result[result.length-1]['id'] this.openedMenus = result.map(v=>v.id) @@ -396,6 +397,27 @@ layout("/layouts/v4/baseLayout.html"){ // 默认选中 + getMenuOwnerPath() { + const pathname = window.location.pathname + if (pathname === "/platform/qsv/survey/reportPage") { + return "/platform/qsv/survey" + } + if (pathname === "/platform/qsv/new") { + return "/platform/qsv/activity" + } + return pathname + }, + + getMenuOwnerFullPath() { + if (window.location.pathname === "/platform/qsv/survey/reportPage") { + return "/platform/qsv/survey" + } + if (window.location.pathname === "/platform/qsv/new") { + return "/platform/qsv/activity" + } + return getFullSubAppPath() + }, + defaultSelect() { // 查找第一个有href的子菜单 function findFirstChildWithHref(menus) { @@ -445,7 +467,7 @@ layout("/layouts/v4/baseLayout.html"){ // 页面加载时获取到正确的菜单 const pathname = window.location.pathname if (!pathname.startsWith("/platform/v4/subApp")) { - const path = getFullSubAppPath() + const path = this.getMenuOwnerFullPath() this.$axios.post("/platform/sys/user/rootMenuByPath", {pathname: path}).then((res) => { if (res.code === 0) { this.setAppInfo(res.data) diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/basicForm.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/basicForm.js index 86a9eec5..187a63bc 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/basicForm.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/basicForm.js @@ -58,6 +58,17 @@ const basicForm = { + + + + @@ -204,7 +204,8 @@ const basicForm = { return { dialogVisible: false, formData: { - category: '' + category: '', + cover: '' }, formRules: { category: [{ required: true, message: "请选择类型", trigger: "change" }], @@ -229,7 +230,7 @@ const basicForm = { }) } else { this.formData = { - category: 'QUIZ' + cover: '' } } }, diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/index.html b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/index.html index 7c0ab1ae..641ed626 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/index.html @@ -2,6 +2,382 @@ layout("/layouts/platform.html"){ #--> + +
@@ -10,32 +386,54 @@ layout("/layouts/platform.html"){ - - + + - - 新增 + - + - + + + + @@ -48,22 +446,22 @@ layout("/layouts/platform.html"){ - +
+ + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js index 5367a467..e188f8ff 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js +++ b/src/main/resources/views/platform/zhgh/dayofficework/qsv/activity/setting.js @@ -8,6 +8,20 @@ const setting = { + + + + + + + +