This commit is contained in:
zhouhefeng
2026-06-15 14:18:01 +08:00
parent 449428b762
commit 873213de53
35 changed files with 4044 additions and 701 deletions
@@ -310,7 +310,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> 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");
@@ -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
@@ -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<NutMap> 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<String> 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<String> wrap(String text, float fontSize, float maxWidth, PDType0Font font) throws IOException {
List<String> 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<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId));
List<QsvUserAnswerRecord> selectOptionUsers = answerRecords.stream().filter(ext -> ObjectUtil.isNotNull(ext.getExtJson().get(subjectId, JSONObject.class)) && ext.getExtJson().get(subjectId, JSONObject.class).getJSONArray("optionIds").contains(optionId)).toList();
public Result selectOptionUsers(@Valid String activityId, @Valid String subjectId, @Valid String optionId, String conditions) {
List<NutMap> selectOptionUsers = qsvSurveyService.selectOptionUsers(activityId, subjectId, optionId, conditions);
return Result.success(selectOptionUsers);
}
@@ -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<NutMap> rows = pagination.getList();
if (rows == null || rows.isEmpty()) {
return;
}
List<String> activityIds = rows.stream()
.map(this::getRowActivityId)
.filter(id -> id != null && !id.isBlank())
.collect(Collectors.toList());
if (activityIds.isEmpty()) {
return;
}
Set<String> 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 "已完成";
}
}
@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<QsvSubject> 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<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
if (subjectIds == null || subjectIds.isEmpty()) {
return new ArrayList<>();
}
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
Map<String, QsvSubject> 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());
}
}
@@ -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<QsvSubject> 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<QsvSubject> 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<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", answerRecord2.getSubjectIds()).asc("sortNum"));
List<QsvSubject> 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<QsvSubject> activitySubjects) {
if (answerRecord == null || Boolean.TRUE.equals(answerRecord.getIsFinish())) {
return;
}
List<String> currentSubjectIds = activitySubjects.stream().map(QsvSubject::getId).collect(Collectors.toList());
List<String> recordSubjectIds = answerRecord.getSubjectIds();
if (recordSubjectIds == null) {
recordSubjectIds = new ArrayList<>();
}
JSONObject extJson = answerRecord.getExtJson();
if (extJson == null) {
extJson = new JSONObject();
}
LinkedHashSet<String> currentSubjectIdSet = new LinkedHashSet<>(currentSubjectIds);
List<String> 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<QsvSubject> querySubjectsByRecordOrder(List<String> subjectIds) {
if (subjectIds == null || subjectIds.isEmpty()) {
return new ArrayList<>();
}
List<QsvSubject> subjects = dao.query(QsvSubject.class, Cnd.where("id", "in", subjectIds));
Map<String, QsvSubject> 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);
}
@@ -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();
}
}
@@ -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)
@@ -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<VisibleRuleCondition> 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<VisibleRuleCondition> children;
}
}
@@ -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<String> userSelectOptionIds;
private String userFillContent;
private Map<String, String> optionFillContents;
}
}
@@ -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> {
QsvActivity saveActivity(QsvActivity qsvActivity);
void deleteActivity(String id);
void updateEnabled(String id, Boolean enabled);
}
@@ -12,6 +12,14 @@ public interface QsvSurveyService extends BaseService<QsvUserAnswerRecord> {
List<NutMap> report(String activityId);
List<NutMap> categoryReport(String activityId, String conditions);
NutMap crossReport(String activityId, String xSubjectIds, String ySubjectIds, String conditions);
NutMap compareReport(String activityId, String subjectId, String optionId);
List<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions);
void exportReportXlsx(String activityId, HttpServletResponse response);
void exportUserAnswerXlsx(String activityId, HttpServletResponse response);
@@ -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<QsvActivity> 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<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", id));
List<String> 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);
}
}
@@ -119,17 +119,11 @@ public class QsvQuizRankServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord>
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);
}
}
@@ -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<QsvUserAnswerRecord> i
// 处理每个题目
for (NutMap subject : subjects) {
// 获取当前题目的选项列表
List<NutMap> subjectOptions = Lang.collection2list(optionsGroup.get(subject.getString("id")), NutMap.class);
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
// 获取题目类型
String subjectType = subject.getString("type");
// 处理文本类型题目
if ("text".equals(subjectType)) {
List<String> texts = answerExtList.stream()
.filter(ext -> ObjectUtil.isNull(ext.get(subject.getString("id"), JSONObject.class)))
.map(ext -> ext.get(subject.getString("id"), JSONObject.class).getStr("text"))
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
List<String> 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<QsvUserAnswerRecord> i
}
@Override
public List<NutMap> categoryReport(String activityId, String conditions) {
if (activityId == null || activityId.isEmpty()) {
throw new IllegalArgumentException("activityId不能为空");
}
try {
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> 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<String> xSubjectIdList = parseSubjectIds(xSubjectIds);
List<String> 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<NutMap> subjects = querySubjects(activityId);
Map<String, NutMap> 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<String> allSubjectIds = new ArrayList<>();
allSubjectIds.addAll(xSubjectIdList);
allSubjectIds.addAll(ySubjectIdList);
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", allSubjectIds).asc("sortNum"));
Map<String, List<QsvOption>> optionGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
List<NutMap> xOptionMaps = buildVariableOptionCombinations(xSubjectIdList, subjectMap, optionGroup);
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, conditionList);
List<NutMap> reports = new ArrayList<>();
for (String ySubjectId : ySubjectIdList) {
NutMap ySubject = subjectMap.get(ySubjectId);
List<QsvOption> yOptions = optionGroup.get(ySubjectId);
if (ObjectUtil.isEmpty(yOptions)) {
yOptions = new ArrayList<>();
}
List<NutMap> yOptionMaps = ObjectUtil.isEmpty(yOptions) ? new ArrayList<>() : Lang.collection2list(yOptions, NutMap.class);
List<NutMap> rows = new ArrayList<>();
for (NutMap xOption : xOptionMaps) {
List<NutMap> xConditions = xOption.getList("conditions", NutMap.class);
List<QsvUserAnswerRecord> xMatchedRecords = filteredRecords.stream()
.filter(record -> isRecordMatched(record, xConditions))
.toList();
long rowTotal = xMatchedRecords.size();
List<NutMap> cells = new ArrayList<>();
for (QsvOption yOption : yOptions) {
List<NutMap> 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<NutMap> subjects = querySubjects(activityId);
Map<String, NutMap> subjectMap = subjects.stream()
.collect(Collectors.toMap(subject -> subject.getString("id"), subject -> subject));
NutMap compareSubject = subjectMap.get(subjectId);
if (compareSubject == null) {
throw new IllegalArgumentException("对比变量不存在");
}
List<String> subjectIds = subjects.stream().map(subject -> subject.getString("id")).toList();
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
Map<String, List<QsvOption>> 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<NutMap> compareCondition = List.of(NutMap.NEW()
.addv("subjectId", subjectId)
.addv("optionId", optionId));
List<QsvUserAnswerRecord> answerRecords = dao().query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
List<QsvUserAnswerRecord> filteredRecords = filterAnswerRecords(answerRecords, compareCondition);
List<NutMap> 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<QsvOption> currentOptions = optionGroup.get(currentSubjectId);
if (ObjectUtil.isEmpty(currentOptions)) {
continue;
}
List<NutMap> cells = new ArrayList<>();
long rowTotal = filteredRecords.size();
for (QsvOption option : currentOptions) {
List<NutMap> 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<NutMap> selectOptionUsers(String activityId, String subjectId, String optionId, String conditions) {
List<NutMap> conditionList = parseCategoryConditions(conditions);
List<QsvUserAnswerRecord> 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<NutMap> report = report(activityId);
@@ -134,10 +357,8 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
exportEntities.add(new ExcelExportEntity("选择人数", "selectCount", 20));
exportEntities.add(new ExcelExportEntity("选择比例", "selectPercent", 20));
Map<String, List<NutMap>> 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<QsvUserAnswerRecord> i
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
@@ -185,7 +406,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
// 构建Excel导出实体
List<ExcelExportEntity> excelExportEntities = buildExcelExportEntities(subjects);
// 构建答题记录列表
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, options);
List<NutMap> list = buildAnswerRecords(answerRecords, subjectMap, optionMap);
// 设置导出参数
ExportParams exportParams = new ExportParams();
@@ -223,7 +444,7 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
// List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
// 查询活动题目列表
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId));
List<QsvSubject> subjects = dao().query(QsvSubject.class, Cnd.where("activityId", "=", activityId).asc("sortNum"));
// 将题目列表转换为Map
Map<String, QsvSubject> subjectMap = subjects.stream().collect(Collectors.toMap(QsvSubject::getId, v -> v));
// 获取题目ID列表
@@ -245,18 +466,15 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> 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<QsvUserAnswerRecord> i
}
}
private List<NutMap> buildReport(String activityId, List<QsvUserAnswerRecord> answerRecords) {
List<JSONObject> answerExtList = answerRecords.stream().map(QsvUserAnswerRecord::getExtJson).toList();
List<NutMap> subjects = querySubjects(activityId);
List<String> subjectIds = subjects.stream().map(v -> v.getString("id")).toList();
List<QsvOption> options = dao().query(QsvOption.class, Cnd.where("subjectId", "in", subjectIds).asc("sortNum"));
Map<String, List<QsvOption>> optionsGroup = options.stream().collect(Collectors.groupingBy(QsvOption::getSubjectId));
for (NutMap subject : subjects) {
List<QsvOption> currentOptions = optionsGroup.get(subject.getString("id"));
List<NutMap> subjectOptions = ObjectUtil.isEmpty(currentOptions) ? new ArrayList<>() : Lang.collection2list(currentOptions, NutMap.class);
String subjectType = subject.getString("type");
if ("text".equals(subjectType)) {
List<NutMap> textAnswers = buildTextAnswerDetails(answerRecords, subject.getString("id"));
List<String> 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<NutMap> 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<String> parseSubjectIds(String subjectIds) {
if (ObjectUtil.isEmpty(subjectIds)) {
return new ArrayList<>();
}
String value = subjectIds.trim();
List<String> 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<String> subjectIds, Map<String, NutMap> 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<NutMap> buildVariableOptionCombinations(List<String> subjectIds, Map<String, NutMap> subjectMap, Map<String, List<QsvOption>> optionGroup) {
List<List<NutMap>> subjectOptionGroups = new ArrayList<>();
for (String subjectId : subjectIds) {
NutMap subject = subjectMap.get(subjectId);
List<QsvOption> options = optionGroup.get(subjectId);
if (ObjectUtil.isEmpty(options)) {
continue;
}
List<NutMap> 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<NutMap> result = new ArrayList<>();
buildVariableOptionCombinations(subjectOptionGroups, 0, new ArrayList<>(), result);
return result;
}
private void buildVariableOptionCombinations(List<List<NutMap>> subjectOptionGroups, int index, List<NutMap> current, List<NutMap> 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<NutMap> 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<QsvUserAnswerRecord> filterAnswerRecords(List<QsvUserAnswerRecord> answerRecords, List<NutMap> conditions) {
if (ObjectUtil.isEmpty(conditions)) {
return answerRecords;
}
return answerRecords.stream()
.filter(record -> isRecordMatched(record, conditions))
.toList();
}
private boolean isRecordMatched(QsvUserAnswerRecord record, List<NutMap> 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<NutMap> querySubjects(String activityId) {
Sql subjectSql = Sqls.create("""
SELECT
@@ -309,7 +692,37 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> i
return excelExportEntities;
}
private List<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, List<QsvOption> options) {
/**
* 构建填空题填写详情。分析弹窗需要同时展示填写内容和答题人信息,
* 因此这里从已完成答题记录中提取当前题目的文本答案,并携带姓名、工号、所属工会和单位。
*
* @param answerRecords 当前活动已完成的答题记录
* @param subjectId 当前填空题ID,用于从 extJson 中取出该题填写内容
* @return 填空题填写详情列表,每条包含 loginName、userName、unionName、unitName、submitTime、text
*/
private List<NutMap> buildTextAnswerDetails(List<QsvUserAnswerRecord> answerRecords, String subjectId) {
List<NutMap> 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<NutMap> buildAnswerRecords(List<QsvUserAnswerRecord> answerRecords, Map<String, QsvSubject> subjectMap, Map<String, QsvOption> optionMap) {
return answerRecords.stream().map(record -> {
NutMap map = NutMap.NEW()
.addv("id", record.getId())
@@ -318,20 +731,61 @@ public class QsvSurveyServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> 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<String, QsvOption> 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<String> 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);
}
}
@@ -121,11 +121,12 @@ public class QsvUserAnswerRecordServiceImpl extends BaseServiceImpl<QsvUserAnswe
String mode = activity.getMode();
String scoreMode = activity.getScoreMode();
boolean highestScoreMode = "HIGH".equals(scoreMode) || "HIGHEST".equals(scoreMode);
if (mode.equals("SCHEDULED")) {
processScoreRecords(activityId, SecurityUtil.getUserId(), true, scoreMode.equals("HIGH"));
processScoreRecords(activityId, SecurityUtil.getUserId(), true, highestScoreMode);
} else if (mode.equals("REGULAR")) {
processScoreRecords(activityId, SecurityUtil.getUserId(), false, scoreMode.equals("HIGH"));
processScoreRecords(activityId, SecurityUtil.getUserId(), false, highestScoreMode);
}
}