This commit is contained in:
2026-06-01 17:19:45 +08:00
parent 67e40e36ea
commit 22c82c6751
12 changed files with 748 additions and 115 deletions
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.dayofficework.qsv.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.json.JSONArray;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.annotation.SLog;
@@ -77,8 +78,34 @@ public class QsvSurveyController {
@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();
List<QsvUserAnswerRecord> answerRecords = dao.query(QsvUserAnswerRecord.class, Cnd.where("activityId", "=", activityId)
.and("isFinish", "=", true));
List<NutMap> selectOptionUsers = answerRecords.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();
return Result.success(selectOptionUsers);
}
@@ -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;
@@ -23,6 +25,7 @@ import javax.validation.Valid;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@IocBean
@At("/platform/h5/qsv/survey")
@@ -40,6 +43,7 @@ public class H5QsvSurveyController {
}
@At
@Aop(TransAop.READ_COMMITTED)
public Result subjects(String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
@@ -51,16 +55,17 @@ public class H5QsvSurveyController {
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();
@@ -77,6 +82,48 @@ public class H5QsvSurveyController {
return Result.success(result);
}
/**
* 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题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();
}
boolean changed = !recordSubjectIds.equals(currentSubjectIds);
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(currentSubjectIds);
answerRecord.setExtJson(extJson);
dao.update(answerRecord);
}
}
@At
public Result answerRecord(@Valid String answerRecordId) {
QsvUserAnswerRecord record = dao.fetch(QsvUserAnswerRecord.class, answerRecordId);
@@ -84,6 +131,7 @@ public class H5QsvSurveyController {
}
@At
@Aop(TransAop.READ_COMMITTED)
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
JSONObject extJson = answerRecord.getExtJson();
@@ -93,6 +141,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);
}
@@ -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)
@@ -63,6 +63,26 @@ public class QsvSubject extends BaseModel implements Serializable {
@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 +99,22 @@ 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;
}
}
@@ -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;
}
}
@@ -68,47 +68,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);
@@ -134,10 +134,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 +169,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 +183,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 +221,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 +243,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();
@@ -309,7 +304,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 +343,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);
}
}