提交代码到GIT,修改了满意度调查的问卷
This commit is contained in:
1 parent
c809141195
commit
1c8e15cc0f
43 files changed
+3487
-1606
No files matched your search
+4
-4
@@ -107,16 +107,16 @@ public class HqSatQuestionController extends BaseController {
|
||||
|
||||
/**
|
||||
* 获取题目的选项列表
|
||||
* 注:选项针对所有单选题和多选题通用,不区分具体题目
|
||||
*
|
||||
* @param questionId 题目ID(预留参数,当前不使用)
|
||||
* @param questionId 题目ID
|
||||
*/
|
||||
@SaCheckPermission("hq:satQuestion:query")
|
||||
@GetMapping("/{questionId}/options")
|
||||
public R<List<HqSatOptionVo>> getQuestionOptions(@PathVariable Long questionId) {
|
||||
// 获取所有启用状态的选项
|
||||
// 根据题目ID获取该题目的选项
|
||||
HqSatOptionBo bo = new HqSatOptionBo();
|
||||
bo.setStatus("0"); // 启用状态
|
||||
bo.setQuestionId(questionId); // 使用questionId进行过滤
|
||||
// bo.setStatus("0"); // 启用状态
|
||||
List<HqSatOptionVo> options = hqSatOptionService.queryList(bo);
|
||||
return R.ok(options);
|
||||
}
|
||||
|
||||
+248
-3
@@ -3,6 +3,7 @@ package org.dromara.hq.controller;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
@@ -24,6 +25,7 @@ import org.dromara.hq.service.IHqSatSurveyService;
|
||||
import org.dromara.hq.domain.vo.HqSatSurveyQuestionVo;
|
||||
import org.dromara.hq.domain.vo.HqSatRecordVo;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.hq.domain.vo.HqSatDetailVo;
|
||||
|
||||
/**
|
||||
* 问卷
|
||||
@@ -31,6 +33,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
* @author wuzilong
|
||||
* @date 2025-06-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@@ -189,7 +192,20 @@ public class HqSatSurveyController extends BaseController {
|
||||
@RepeatSubmit()
|
||||
@PostMapping("/answer/submit")
|
||||
public R<HqSatRecordVo> submitSurveyAnswer(@Validated @RequestBody HqSatAnswerSubmitBo submitBo) {
|
||||
return R.ok(hqSatSurveyService.submitSurveyAnswer(submitBo));
|
||||
log.debug("接收到问卷答题提交请求: {}", submitBo);
|
||||
try {
|
||||
// 验证提交数据完整性
|
||||
if (submitBo.getAnswers() == null || submitBo.getAnswers().isEmpty()) {
|
||||
return R.fail("未提交任何答案");
|
||||
}
|
||||
|
||||
// 针对问卷答题记录执行提交操作
|
||||
HqSatRecordVo result = hqSatSurveyService.submitSurveyAnswer(submitBo);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("提交问卷答案失败", e);
|
||||
return R.fail("提交失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,7 +217,20 @@ public class HqSatSurveyController extends BaseController {
|
||||
@RepeatSubmit()
|
||||
@PostMapping("/answer/draft")
|
||||
public R<HqSatRecordVo> saveSurveyDraft(@Validated @RequestBody HqSatAnswerSubmitBo submitBo) {
|
||||
return R.ok(hqSatSurveyService.saveSurveyDraft(submitBo));
|
||||
log.debug("接收到问卷答题草稿保存请求: {}", submitBo);
|
||||
try {
|
||||
// 验证提交数据
|
||||
if (submitBo.getAnswers() == null) {
|
||||
submitBo.setAnswers(List.of()); // 允许保存空草稿
|
||||
}
|
||||
|
||||
// 执行草稿保存操作
|
||||
HqSatRecordVo result = hqSatSurveyService.saveSurveyDraft(submitBo);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("保存问卷答案草稿失败", e);
|
||||
return R.fail("保存草稿失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +250,222 @@ public class HqSatSurveyController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/{surveyId}/answer")
|
||||
public R<HqSatRecordVo> getUserSurveyAnswer(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
|
||||
return R.ok(hqSatSurveyService.getUserSurveyAnswer(surveyId));
|
||||
try {
|
||||
HqSatRecordVo recordVo = hqSatSurveyService.getUserSurveyAnswer(surveyId);
|
||||
|
||||
// 确保返回的数据结构符合前端预期
|
||||
if (recordVo != null && recordVo.getDetails() != null) {
|
||||
List<HqSatDetailVo> expandedDetails = new java.util.ArrayList<>();
|
||||
|
||||
// 记录已存在,确保每个detail都有正确的字段格式
|
||||
for (var detail : recordVo.getDetails()) {
|
||||
// 确保answerScore字段存在,frontend期望这个字段
|
||||
if (detail.getAnswerScore() == null && detail.getAnswer_score() != null) {
|
||||
detail.setAnswerScore(detail.getAnswer_score());
|
||||
}
|
||||
|
||||
// 处理多选题 - 每个选项应该是单独的一条记录
|
||||
if ("2".equals(detail.getQuestionType()) || "2".equals(String.valueOf(detail.getQuestionType()))) {
|
||||
// 检查是否有多个选项(通常用逗号或其他分隔符分隔)
|
||||
if (detail.getAnswerValue() != null && detail.getAnswerValue().contains(",")) {
|
||||
String[] selectedValues = detail.getAnswerValue().split(",");
|
||||
|
||||
// 如果有options数据,优先使用它来匹配选项ID
|
||||
if (detail.getOptions() != null && !detail.getOptions().isEmpty()) {
|
||||
// 有options数据的情况下,通过选项值匹配选项ID
|
||||
java.util.Map<String, Long> valueToIdMap = new java.util.HashMap<>();
|
||||
for (var option : detail.getOptions()) {
|
||||
if (option != null && option.getOptionValue() != null) {
|
||||
valueToIdMap.put(option.getOptionValue(), option.getOptionId());
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个选项创建独立的记录
|
||||
for (String optionValue : selectedValues) {
|
||||
HqSatDetailVo newDetail = new HqSatDetailVo();
|
||||
|
||||
// 复制所有基本属性
|
||||
copyDetailProperties(detail, newDetail);
|
||||
|
||||
// 设置单独的选项值
|
||||
String trimmedValue = optionValue.trim();
|
||||
newDetail.setAnswerValue(trimmedValue);
|
||||
|
||||
// 设置对应的选项ID(如果存在)
|
||||
if (valueToIdMap.containsKey(trimmedValue)) {
|
||||
newDetail.setOptionId(valueToIdMap.get(trimmedValue));
|
||||
}
|
||||
|
||||
expandedDetails.add(newDetail);
|
||||
}
|
||||
} else {
|
||||
// 没有options数据的情况下,只能按顺序拆分
|
||||
// 尝试拆分optionId(如果是用逗号分隔的多个ID)
|
||||
Long[] optionIds = null;
|
||||
if (detail.getOptionId() != null) {
|
||||
try {
|
||||
String optionIdStr = String.valueOf(detail.getOptionId());
|
||||
if (optionIdStr.contains(",")) {
|
||||
String[] optionIdParts = optionIdStr.split(",");
|
||||
optionIds = new Long[optionIdParts.length];
|
||||
for (int i = 0; i < optionIdParts.length; i++) {
|
||||
optionIds[i] = Long.valueOf(optionIdParts[i].trim());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("拆分多选题选项ID失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个选项创建独立的记录
|
||||
for (int i = 0; i < selectedValues.length; i++) {
|
||||
HqSatDetailVo newDetail = new HqSatDetailVo();
|
||||
|
||||
// 复制所有基本属性
|
||||
copyDetailProperties(detail, newDetail);
|
||||
|
||||
// 设置单独的选项值
|
||||
String trimmedValue = selectedValues[i].trim();
|
||||
newDetail.setAnswerValue(trimmedValue);
|
||||
|
||||
// 如果有对应的optionId,则设置
|
||||
if (optionIds != null && i < optionIds.length) {
|
||||
newDetail.setOptionId(optionIds[i]);
|
||||
}
|
||||
|
||||
expandedDetails.add(newDetail);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 单个选项的多选题或格式不符合预期,保持不变
|
||||
expandedDetails.add(detail);
|
||||
}
|
||||
} else {
|
||||
// 确保问答题的remark字段可被前端正确识别
|
||||
if ("3".equals(detail.getQuestionType()) || "3".equals(String.valueOf(detail.getQuestionType()))) {
|
||||
if (detail.getRemark() == null || detail.getRemark().isEmpty()) {
|
||||
// 如果remark为空但answerValue有内容,则移动到remark
|
||||
if (detail.getAnswerValue() != null && !detail.getAnswerValue().isEmpty()) {
|
||||
detail.setRemark(detail.getAnswerValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
expandedDetails.add(detail);
|
||||
}
|
||||
}
|
||||
|
||||
// 用展开后的列表替换原始详情列表
|
||||
recordVo.setDetails(expandedDetails);
|
||||
}
|
||||
|
||||
return R.ok(recordVo);
|
||||
} catch (Exception e) {
|
||||
// 日志记录异常
|
||||
log.error("获取用户问卷答题记录失败", e);
|
||||
// 若是因为用户未答题导致的异常,返回更友好的提示
|
||||
if (e.getMessage() != null && e.getMessage().contains("未找到有效的答题记录")) {
|
||||
return R.fail(404, "未找到有效的答题记录");
|
||||
}
|
||||
return R.fail("获取答题记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制DetailVo对象的属性(用于多选题拆分)
|
||||
*
|
||||
* @param source 源对象
|
||||
* @param target 目标对象
|
||||
*/
|
||||
private void copyDetailProperties(HqSatDetailVo source, HqSatDetailVo target) {
|
||||
// 复制基本属性,但不复制会变化的属性(如answerValue和optionId)
|
||||
target.setId(source.getId());
|
||||
target.setDeleted(source.getDeleted());
|
||||
target.setRecordId(source.getRecordId());
|
||||
target.setQuestionId(source.getQuestionId());
|
||||
target.setQuestionType(source.getQuestionType());
|
||||
target.setAnswerScore(source.getAnswerScore());
|
||||
target.setRemark(source.getRemark());
|
||||
|
||||
// 不复制:answerValue - 这将单独设置
|
||||
// 不复制:optionId - 这将单独设置
|
||||
|
||||
// 对于嵌套对象,如果需要,可以进行深拷贝
|
||||
// 这里不复制options,因为这是只读数据,不需要为每个拆分记录保留
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷统计分析数据
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
*/
|
||||
@SaCheckPermission("hq:satSurvey:result")
|
||||
@GetMapping("/{surveyId}/statistics")
|
||||
public R<java.util.Map<String, Object>> getSurveyStatistics(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
|
||||
return R.ok(hqSatSurveyService.getSurveyStatistics(surveyId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷的所有答题记录列表
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param status 答题状态
|
||||
* @param userName 答题人姓名
|
||||
* @param dateRange 答题时间范围
|
||||
* @param pageNum 页码
|
||||
* @param pageSize 每页大小
|
||||
*/
|
||||
@SaCheckPermission("hq:satSurvey:result")
|
||||
@GetMapping("/{surveyId}/records")
|
||||
public R<java.util.Map<String, Object>> getSurveyAnswerRecords(
|
||||
@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String userName,
|
||||
@RequestParam(required = false) String[] dateRange,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
|
||||
java.util.Map<String, Object> query = new java.util.HashMap<>();
|
||||
query.put("status", status);
|
||||
query.put("userName", userName);
|
||||
query.put("dateRange", dateRange);
|
||||
query.put("pageNum", pageNum);
|
||||
query.put("pageSize", pageSize);
|
||||
|
||||
return R.ok(hqSatSurveyService.getSurveyAnswerRecords(surveyId, query));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看答题记录详情
|
||||
*
|
||||
* @param recordId 答题记录ID
|
||||
*/
|
||||
@SaCheckPermission("hq:satSurvey:result")
|
||||
@GetMapping("/record/{recordId}")
|
||||
public R<java.util.Map<String, Object>> getAnswerRecordDetail(@NotNull(message = "记录ID不能为空") @PathVariable Long recordId) {
|
||||
return R.ok(hqSatSurveyService.getAnswerRecordDetail(recordId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出调查报告
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
*/
|
||||
@SaCheckPermission("hq:satSurvey:export")
|
||||
@Log(title = "导出调查报告", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/{surveyId}/exportReport")
|
||||
public void exportSurveyReport(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId, HttpServletResponse response) {
|
||||
hqSatSurveyService.exportSurveyReport(surveyId, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出原始数据
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
*/
|
||||
@SaCheckPermission("hq:satSurvey:export")
|
||||
@Log(title = "导出原始数据", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/{surveyId}/exportRawData")
|
||||
public void exportSurveyRawData(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId, HttpServletResponse response) {
|
||||
hqSatSurveyService.exportSurveyRawData(surveyId, response);
|
||||
}
|
||||
}
|
||||
@@ -52,5 +52,10 @@ public class HqSatDetail extends TenantEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目选项ID
|
||||
*/
|
||||
private Long optionId;
|
||||
|
||||
|
||||
}
|
||||
+18
-13
@@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 评分选项对象 hq_sat_option
|
||||
* 满意度调查题目选项对象 hq_sat_option
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-06-24
|
||||
@@ -27,35 +27,40 @@ public class HqSatOption extends TenantEntity {
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 题目ID
|
||||
*/
|
||||
private Long questionId;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
*/
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
private Long deleted;
|
||||
private String deleted;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 选项内容
|
||||
*/
|
||||
private String optionText;
|
||||
private String optionContent;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
private Long optionValue;
|
||||
private Long optionScore;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
private Long sortOrder;
|
||||
private Long optionOrder;
|
||||
|
||||
/**
|
||||
* 是否默认选项(Y是 N否)
|
||||
*/
|
||||
private String isDefault;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
// private String status;
|
||||
|
||||
}
|
||||
@@ -73,4 +73,9 @@ public class HqSatQuestion extends TenantEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目分值
|
||||
*/
|
||||
private Long score;
|
||||
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public class HqSatRecord extends TenantEntity {
|
||||
/**
|
||||
* 平均分
|
||||
*/
|
||||
private Long avgScore;
|
||||
private java.math.BigDecimal avgScore;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
|
||||
@@ -95,5 +95,9 @@ public class HqSatSurvey extends TenantEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
private Long score;
|
||||
|
||||
}
|
||||
+29
@@ -1,5 +1,6 @@
|
||||
package org.dromara.hq.domain.bo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -49,6 +50,8 @@ public class HqSatAnswerSubmitBo {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 单个答案项
|
||||
*/
|
||||
@@ -76,6 +79,32 @@ public class HqSatAnswerSubmitBo {
|
||||
* 选项详情(单选题和多选题需要)
|
||||
*/
|
||||
private List<OptionDetail> options;
|
||||
|
||||
/**
|
||||
* 题目选项ID
|
||||
*/
|
||||
@NotNull(message = "题目选项ID不能为空")
|
||||
@JsonProperty(value = "optionId", access = JsonProperty.Access.WRITE_ONLY)
|
||||
private Long optionId;
|
||||
|
||||
/**
|
||||
* 兼容前端传入的option_id
|
||||
*/
|
||||
@JsonProperty("option_id")
|
||||
public void setOptionIdFromSnakeCase(Long optionId) {
|
||||
this.optionId = optionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 题目选项分值
|
||||
*/
|
||||
private Long answerScore;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
@@ -53,5 +53,11 @@ public class HqSatDetailBo extends BaseEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目选项ID
|
||||
*/
|
||||
@NotNull(message = "题目选项ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long optionId;
|
||||
|
||||
|
||||
}
|
||||
+21
-16
@@ -10,7 +10,7 @@ import lombok.EqualsAndHashCode;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 评分选项业务对象 hq_sat_option
|
||||
* 满意度调查题目选项业务对象 hq_sat_option
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-06-24
|
||||
@@ -27,36 +27,41 @@ public class HqSatOptionBo extends BaseEntity {
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
* 题目ID
|
||||
*/
|
||||
private Long deleted;
|
||||
@NotNull(message = "题目ID不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long questionId;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 删除标志
|
||||
*/
|
||||
@NotBlank(message = "选项文本不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String optionText;
|
||||
private String deleted;
|
||||
|
||||
/**
|
||||
* 选项内容
|
||||
*/
|
||||
@NotBlank(message = "选项内容不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private String optionContent;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
@NotNull(message = "选项分值不能为空", groups = { AddGroup.class, EditGroup.class })
|
||||
private Long optionValue;
|
||||
private Long optionScore;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
private Long sortOrder;
|
||||
private Long optionOrder;
|
||||
|
||||
/**
|
||||
* 是否默认选项(Y是 N否)
|
||||
*/
|
||||
private String isDefault;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
// private String status;
|
||||
|
||||
}
|
||||
+4
@@ -73,5 +73,9 @@ public class HqSatQuestionBo extends BaseEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目分值
|
||||
*/
|
||||
private Long score;
|
||||
|
||||
}
|
||||
+6
-1
@@ -9,6 +9,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
/**
|
||||
@@ -69,12 +70,16 @@ public class HqSatRecordBo extends BaseEntity {
|
||||
/**
|
||||
* 平均分
|
||||
*/
|
||||
private Long avgScore;
|
||||
private java.math.BigDecimal avgScore;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 答题详情列表
|
||||
*/
|
||||
private List<HqSatDetailBo> details;
|
||||
|
||||
}
|
||||
+5
@@ -123,4 +123,9 @@ public class HqSatSurveyBo extends BaseEntity {
|
||||
*/
|
||||
private List<String> statusIn;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
private Long score;
|
||||
|
||||
}
|
||||
+36
-2
@@ -11,8 +11,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 答题详情视图对象 hq_sat_detail
|
||||
@@ -53,17 +52,52 @@ public class HqSatDetailVo implements Serializable {
|
||||
@ExcelProperty(value = "题目ID")
|
||||
private Long questionId;
|
||||
|
||||
/**
|
||||
* 题目类型 (1-单选题 2-多选题 3-问答题)
|
||||
*/
|
||||
private String questionType;
|
||||
|
||||
/**
|
||||
* 答案分数
|
||||
*/
|
||||
@ExcelProperty(value = "答案分数")
|
||||
private Long answerScore;
|
||||
|
||||
/**
|
||||
* 答案值 (兼容旧版本字段)
|
||||
*/
|
||||
private String answerValue;
|
||||
|
||||
/**
|
||||
* 兼容旧字段 answer_score
|
||||
*/
|
||||
private Long answer_score;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目选项ID
|
||||
*/
|
||||
@ExcelProperty(value = "题目选项ID")
|
||||
private Long optionId;
|
||||
|
||||
/**
|
||||
* 选项信息列表 (用于单选题和多选题)
|
||||
*/
|
||||
private List<OptionInfo> options;
|
||||
|
||||
/**
|
||||
* 选项信息对象
|
||||
*/
|
||||
@Data
|
||||
public static class OptionInfo implements Serializable {
|
||||
private Long optionId;
|
||||
private String optionValue;
|
||||
private String optionText;
|
||||
private Boolean isSelected;
|
||||
}
|
||||
}
|
||||
+45
-15
@@ -15,7 +15,7 @@ import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* 评分选项视图对象 hq_sat_option
|
||||
* 满意度调查题目选项视图对象 hq_sat_option
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-06-24
|
||||
@@ -34,43 +34,73 @@ public class HqSatOptionVo implements Serializable {
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 题目ID
|
||||
*/
|
||||
@ExcelProperty(value = "题目ID")
|
||||
private Long questionId;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
*/
|
||||
@ExcelProperty(value = "删除标志", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_item_delete")
|
||||
private Long deleted;
|
||||
private String deleted;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 选项内容
|
||||
*/
|
||||
@ExcelProperty(value = "选项文本")
|
||||
private String optionText;
|
||||
@ExcelProperty(value = "选项内容")
|
||||
private String optionContent;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
@ExcelProperty(value = "选项分值")
|
||||
private Long optionValue;
|
||||
private Long optionScore;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
@ExcelProperty(value = "排序")
|
||||
private Long sortOrder;
|
||||
@ExcelProperty(value = "选项排序")
|
||||
private Long optionOrder;
|
||||
|
||||
/**
|
||||
* 是否默认选项
|
||||
*/
|
||||
@ExcelProperty(value = "是否默认选项", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_yes_no")
|
||||
private String isDefault;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_item_enable")
|
||||
private String status;
|
||||
// @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
|
||||
// @ExcelDictFormat(dictType = "sys_item_enable")
|
||||
// private String status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
* 创建者
|
||||
*/
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
@ExcelProperty(value = "创建者")
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@ExcelProperty(value = "更新者")
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
+6
@@ -92,5 +92,11 @@ public class HqSatQuestionVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 题目分值
|
||||
*/
|
||||
@ExcelProperty(value = "题目分值")
|
||||
private Long score;
|
||||
|
||||
|
||||
}
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
package org.dromara.hq.domain.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.dromara.hq.domain.HqSatRecord;
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
@@ -83,7 +84,7 @@ public class HqSatRecordVo implements Serializable {
|
||||
* 平均分
|
||||
*/
|
||||
@ExcelProperty(value = "平均分")
|
||||
private Long avgScore;
|
||||
private java.math.BigDecimal avgScore;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
@@ -91,5 +92,9 @@ public class HqSatRecordVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 答题详情列表
|
||||
*/
|
||||
private List<HqSatDetailVo> details;
|
||||
|
||||
}
|
||||
+5
@@ -119,5 +119,10 @@ public class HqSatSurveyVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
@ExcelProperty(value = "问卷总分值")
|
||||
private Long score;
|
||||
|
||||
}
|
||||
+41
@@ -133,4 +133,45 @@ public interface IHqSatSurveyService {
|
||||
* @return 答题记录
|
||||
*/
|
||||
HqSatRecordVo getUserSurveyAnswer(Long surveyId);
|
||||
|
||||
/**
|
||||
* 获取问卷统计分析数据
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @return 统计分析数据
|
||||
*/
|
||||
java.util.Map<String, Object> getSurveyStatistics(Long surveyId);
|
||||
|
||||
/**
|
||||
* 获取问卷的所有答题记录列表
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param query 查询参数
|
||||
* @return 答题记录列表
|
||||
*/
|
||||
java.util.Map<String, Object> getSurveyAnswerRecords(Long surveyId, java.util.Map<String, Object> query);
|
||||
|
||||
/**
|
||||
* 查看答题记录详情
|
||||
*
|
||||
* @param recordId 答题记录ID
|
||||
* @return 答题记录详情
|
||||
*/
|
||||
java.util.Map<String, Object> getAnswerRecordDetail(Long recordId);
|
||||
|
||||
/**
|
||||
* 导出调查报告
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
void exportSurveyReport(Long surveyId, jakarta.servlet.http.HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 导出原始数据
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
void exportSurveyRawData(Long surveyId, jakarta.servlet.http.HttpServletResponse response);
|
||||
}
|
||||
+20
-17
@@ -21,7 +21,7 @@ import java.util.Map;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 评分选项Service业务层处理
|
||||
* 满意度调查题目选项Service业务层处理
|
||||
*
|
||||
* @author Lion Li
|
||||
* @date 2025-06-24
|
||||
@@ -34,10 +34,10 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
private final HqSatOptionMapper baseMapper;
|
||||
|
||||
/**
|
||||
* 查询评分选项
|
||||
* 查询满意度调查题目选项
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 评分选项
|
||||
* @return 满意度调查题目选项
|
||||
*/
|
||||
@Override
|
||||
public HqSatOptionVo queryById(Long id) {
|
||||
@@ -45,11 +45,11 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询评分选项列表
|
||||
* 分页查询满意度调查题目选项列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 评分选项分页列表
|
||||
* @return 满意度调查题目选项分页列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<HqSatOptionVo> queryPageList(HqSatOptionBo bo, PageQuery pageQuery) {
|
||||
@@ -59,10 +59,10 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询符合条件的评分选项列表
|
||||
* 查询符合条件的满意度调查题目选项列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 评分选项列表
|
||||
* @return 满意度调查题目选项列表
|
||||
*/
|
||||
@Override
|
||||
public List<HqSatOptionVo> queryList(HqSatOptionBo bo) {
|
||||
@@ -73,19 +73,22 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
private LambdaQueryWrapper<HqSatOption> buildQueryWrapper(HqSatOptionBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<HqSatOption> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(HqSatOption::getId);
|
||||
// 先按选项排序字段升序排序,再按ID排序
|
||||
lqw.orderByAsc(HqSatOption::getOptionOrder).orderByAsc(HqSatOption::getId);
|
||||
lqw.eq(bo.getDeleted() != null, HqSatOption::getDeleted, bo.getDeleted());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getOptionText()), HqSatOption::getOptionText, bo.getOptionText());
|
||||
lqw.eq(bo.getOptionValue() != null, HqSatOption::getOptionValue, bo.getOptionValue());
|
||||
lqw.eq(bo.getSortOrder() != null, HqSatOption::getSortOrder, bo.getSortOrder());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), HqSatOption::getStatus, bo.getStatus());
|
||||
lqw.eq(bo.getQuestionId() != null, HqSatOption::getQuestionId, bo.getQuestionId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getOptionContent()), HqSatOption::getOptionContent, bo.getOptionContent());
|
||||
lqw.eq(bo.getOptionScore() != null, HqSatOption::getOptionScore, bo.getOptionScore());
|
||||
lqw.eq(bo.getOptionOrder() != null, HqSatOption::getOptionOrder, bo.getOptionOrder());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getIsDefault()), HqSatOption::getIsDefault, bo.getIsDefault());
|
||||
// lqw.eq(StringUtils.isNotBlank(bo.getStatus()), HqSatOption::getStatus, bo.getStatus());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评分选项
|
||||
* 新增满意度调查题目选项
|
||||
*
|
||||
* @param bo 评分选项
|
||||
* @param bo 满意度调查题目选项
|
||||
* @return 是否新增成功
|
||||
*/
|
||||
@Override
|
||||
@@ -100,9 +103,9 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改评分选项
|
||||
* 修改满意度调查题目选项
|
||||
*
|
||||
* @param bo 评分选项
|
||||
* @param bo 满意度调查题目选项
|
||||
* @return 是否修改成功
|
||||
*/
|
||||
@Override
|
||||
@@ -120,7 +123,7 @@ public class HqSatOptionServiceImpl implements IHqSatOptionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除评分选项信息
|
||||
* 校验并批量删除满意度调查题目选项信息
|
||||
*
|
||||
* @param ids 待删除的主键集合
|
||||
* @param isValid 是否进行有效性校验
|
||||
|
||||
+2
@@ -74,6 +74,8 @@ public class HqSatQuestionServiceImpl implements IHqSatQuestionService {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<HqSatQuestion> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(HqSatQuestion::getId);
|
||||
// 添加ID查询条件
|
||||
lqw.eq(bo.getId() != null, HqSatQuestion::getId, bo.getId());
|
||||
lqw.eq(bo.getDeleted() != null, HqSatQuestion::getDeleted, bo.getDeleted());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getQuestionTitle()), HqSatQuestion::getQuestionTitle, bo.getQuestionTitle());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getQuestionContent()), HqSatQuestion::getQuestionContent,
|
||||
|
||||
+621
-73
@@ -432,8 +432,8 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
recordBo.setUserId(userId);
|
||||
recordBo.setUserType(userType);
|
||||
recordBo.setSubmitTime(new Date());
|
||||
recordBo.setTotalScore(0L); // 暂时设为0,后续可以计算
|
||||
recordBo.setAvgScore(0L);
|
||||
recordBo.setTotalScore(0L); // 初始化为0,后续计算
|
||||
recordBo.setAvgScore(java.math.BigDecimal.ZERO); // 初始化为0,后续计算
|
||||
recordBo.setRemark(submitBo.getRemark());
|
||||
|
||||
// 保存答题记录
|
||||
@@ -442,17 +442,48 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
throw new ServiceException("保存答题记录失败");
|
||||
}
|
||||
|
||||
// 用于统计分数
|
||||
long totalScore = 0L;
|
||||
int scoredQuestionCount = 0; // 有分值的题目数量(单选题+多选题)
|
||||
|
||||
// 已计数的题目ID集合,用于确保多选题只计数一次
|
||||
java.util.Set<Long> countedQuestionIds = new java.util.HashSet<>();
|
||||
|
||||
// 保存答题详情
|
||||
for (HqSatAnswerSubmitBo.AnswerItem answerItem : submitBo.getAnswers()) {
|
||||
HqSatDetailBo detailBo = new HqSatDetailBo();
|
||||
detailBo.setRecordId(recordBo.getId());
|
||||
detailBo.setQuestionId(answerItem.getQuestionId());
|
||||
detailBo.setAnswerScore(0L); // 暂时设为0,后续可以计算分数
|
||||
|
||||
// 计算题目得分
|
||||
long questionScore = 0L;
|
||||
String questionType = answerItem.getQuestionType();
|
||||
|
||||
// 单选题和多选题需要计算分数
|
||||
if ("1".equals(questionType) || "2".equals(questionType)) {
|
||||
questionScore = answerItem.getAnswerScore();
|
||||
// 如果题目ID尚未计数,则增加有分值的题目数量
|
||||
if (!countedQuestionIds.contains(answerItem.getQuestionId())) {
|
||||
scoredQuestionCount++;
|
||||
countedQuestionIds.add(answerItem.getQuestionId());
|
||||
}
|
||||
}
|
||||
// 问答题不计分,分值为0
|
||||
|
||||
// 设置题目分数
|
||||
detailBo.setAnswerScore(questionScore);
|
||||
|
||||
// 累加到总分
|
||||
totalScore += questionScore;
|
||||
|
||||
log.info("题目ID: {}, 类型: {}, 得分: {}",
|
||||
answerItem.getQuestionId(), questionType, questionScore);
|
||||
|
||||
// 将答案内容临时存储在remark字段中
|
||||
String answerContent = buildAnswerContent(answerItem);
|
||||
detailBo.setRemark(answerContent);
|
||||
// String answerContent = buildAnswerContent(answerItem);
|
||||
detailBo.setRemark(answerItem.getRemark());
|
||||
|
||||
detailBo.setOptionId(answerItem.getOptionId());
|
||||
boolean detailSaved = detailService.insertByBo(detailBo);
|
||||
if (!detailSaved) {
|
||||
log.error("保存答题详情失败,题目ID: {}", answerItem.getQuestionId());
|
||||
@@ -462,6 +493,28 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
saveAnswerDetails(recordBo.getId(), answerItem);
|
||||
}
|
||||
|
||||
// 计算平均分
|
||||
java.math.BigDecimal avgScore = java.math.BigDecimal.ZERO;
|
||||
if (scoredQuestionCount > 0) {
|
||||
avgScore = new java.math.BigDecimal(totalScore)
|
||||
.divide(new java.math.BigDecimal(scoredQuestionCount), 2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
log.info("问卷总分: {}, 平均分: {}, 有分值题目数量: {}",
|
||||
totalScore, avgScore, scoredQuestionCount);
|
||||
|
||||
// 更新答题记录的总分和平均分
|
||||
recordBo.setTotalScore(totalScore);
|
||||
recordBo.setAvgScore(avgScore);
|
||||
boolean updated = recordService.updateByBo(recordBo);
|
||||
|
||||
if (!updated) {
|
||||
log.warn("更新答题记录分数失败,记录ID: {}", recordBo.getId());
|
||||
} else {
|
||||
log.info("成功更新答题记录分数,记录ID: {}, 总分: {}, 平均分: {}",
|
||||
recordBo.getId(), totalScore, avgScore);
|
||||
}
|
||||
|
||||
// 返回答题记录
|
||||
return recordService.queryById(recordBo.getId());
|
||||
}
|
||||
@@ -743,58 +796,498 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 hq_sat_answer 表查询答案详情
|
||||
* 获取问卷统计分析数据
|
||||
*
|
||||
* @param recordId 答题记录ID
|
||||
* @return 答案详情列表
|
||||
* @param surveyId 问卷ID
|
||||
* @return 统计分析数据
|
||||
*/
|
||||
private List<Map<String, Object>> getAnswersFromSatAnswerTable(Long recordId) {
|
||||
@Override
|
||||
public java.util.Map<String, Object> getSurveyStatistics(Long surveyId) {
|
||||
log.info("开始获取问卷统计数据,问卷ID: {}", surveyId);
|
||||
|
||||
try {
|
||||
log.debug("从 hq_sat_answer 表查询答案,记录ID: {}", recordId);
|
||||
|
||||
// 查询该记录的所有答案
|
||||
String answerSql = """
|
||||
SELECT question_id, question_type, answer_value,
|
||||
option_id, option_value, option_text, answer_score
|
||||
FROM hq_sat_answer
|
||||
WHERE record_id = ? AND deleted = 0
|
||||
ORDER BY question_id, option_id
|
||||
""";
|
||||
|
||||
List<Map<String, Object>> answerResults = jdbcTemplate.queryForList(answerSql, recordId);
|
||||
log.debug("查询到原始答案数据: {} 条", answerResults.size());
|
||||
|
||||
// 按题目ID分组聚合答案
|
||||
Map<Long, Map<String, Object>> questionAnswerMap = new HashMap<>();
|
||||
|
||||
for (Map<String, Object> answerRow : answerResults) {
|
||||
Long questionId = Long.valueOf(answerRow.get("question_id").toString());
|
||||
String questionType = answerRow.get("question_type").toString();
|
||||
String answerValue = (String) answerRow.get("answer_value");
|
||||
|
||||
// 获取或创建题目答案对象
|
||||
Map<String, Object> questionAnswer = questionAnswerMap.computeIfAbsent(questionId, k -> {
|
||||
Map<String, Object> answer = new HashMap<>();
|
||||
answer.put("questionId", questionId);
|
||||
answer.put("questionType", questionType);
|
||||
answer.put("answerValue", answerValue);
|
||||
return answer;
|
||||
});
|
||||
|
||||
log.debug("处理题目{}的答案: questionType={}, answerValue={}",
|
||||
questionId, questionType, answerValue);
|
||||
// 查询问卷基本信息
|
||||
HqSatSurvey survey = baseMapper.selectById(surveyId);
|
||||
if (survey == null) {
|
||||
throw new ServiceException("问卷不存在", 404);
|
||||
}
|
||||
|
||||
// 转换为列表并返回
|
||||
List<Map<String, Object>> result = new ArrayList<>(questionAnswerMap.values());
|
||||
log.debug("聚合后的答案数据: {} 个题目", result.size());
|
||||
// 获取统计数据
|
||||
java.util.Map<String, Object> statistics = new java.util.HashMap<>();
|
||||
|
||||
// 基本统计信息
|
||||
String totalParticipantsSql = """
|
||||
SELECT COUNT(DISTINCT u.user_id)
|
||||
FROM sys_user u
|
||||
INNER JOIN sys_user_role ur ON u.user_id = ur.user_id
|
||||
INNER JOIN sys_role r ON ur.role_id = r.role_id
|
||||
WHERE u.del_flag = '0' AND
|
||||
CASE
|
||||
WHEN ? = '1' THEN r.role_key = 'student'
|
||||
WHEN ? = '2' THEN r.role_key = 'teacher'
|
||||
WHEN ? = '3' THEN (r.role_key = 'student' OR r.role_key = 'teacher')
|
||||
ELSE 1=0
|
||||
END""";
|
||||
// 执行查询时传入三次相同的targetGroup参数
|
||||
Integer totalParticipants = jdbcTemplate.queryForObject(
|
||||
totalParticipantsSql,
|
||||
Integer.class,
|
||||
survey.getTargetGroup(), survey.getTargetGroup(), survey.getTargetGroup());
|
||||
|
||||
String completedCountSql = "SELECT COUNT(*) FROM hq_sat_record WHERE survey_id = ? AND deleted = 0 AND submit_time IS NOT NULL";
|
||||
Integer completedCount = jdbcTemplate.queryForObject(completedCountSql, Integer.class, surveyId);
|
||||
|
||||
String draftCountSql = "SELECT COUNT(*) FROM hq_sat_record WHERE survey_id = ? AND deleted = 0 AND submit_time IS NULL";
|
||||
Integer draftCount = jdbcTemplate.queryForObject(draftCountSql, Integer.class, surveyId);
|
||||
|
||||
// 计算完成率
|
||||
double completionRate = totalParticipants > 0 ? (double) completedCount / totalParticipants : 0.0;
|
||||
|
||||
// 平均得分统计
|
||||
String scoreStatsSql = "SELECT AVG(total_score) as avgScore, MAX(total_score) as maxScore, MIN(total_score) as minScore FROM hq_sat_record WHERE survey_id = ? AND deleted = 0 AND total_score IS NOT NULL";
|
||||
java.util.List<java.util.Map<String, Object>> scoreStats = jdbcTemplate.queryForList(scoreStatsSql, surveyId);
|
||||
|
||||
Double avgScore = 0.0;
|
||||
Integer maxScore = 0;
|
||||
Integer minScore = 0;
|
||||
if (!scoreStats.isEmpty() && scoreStats.get(0).get("avgScore") != null) {
|
||||
avgScore = ((Number) scoreStats.get(0).get("avgScore")).doubleValue();
|
||||
maxScore = scoreStats.get(0).get("maxScore") != null ? ((Number) scoreStats.get(0).get("maxScore")).intValue() : 0;
|
||||
minScore = scoreStats.get(0).get("minScore") != null ? ((Number) scoreStats.get(0).get("minScore")).intValue() : 0;
|
||||
}
|
||||
|
||||
// 构建返回数据
|
||||
statistics.put("surveyId", surveyId);
|
||||
statistics.put("surveyTitle", survey.getSurveyTitle());
|
||||
statistics.put("surveyCode", survey.getSurveyCode());
|
||||
statistics.put("totalParticipants", totalParticipants != null ? totalParticipants : 0);
|
||||
statistics.put("completedCount", completedCount != null ? completedCount : 0);
|
||||
statistics.put("draftCount", draftCount != null ? draftCount : 0);
|
||||
statistics.put("completionRate", completionRate);
|
||||
// 计算平均填写时长(分钟)
|
||||
String durationSql = """
|
||||
SELECT AVG(TIMESTAMPDIFF(MINUTE, create_time, submit_time)) as avgDuration
|
||||
FROM hq_sat_record
|
||||
WHERE survey_id = ? AND deleted = 0
|
||||
AND submit_time IS NOT NULL
|
||||
AND create_time IS NOT NULL
|
||||
AND TIMESTAMPDIFF(MINUTE, create_time, submit_time) BETWEEN 1 AND 1440
|
||||
""";
|
||||
Double avgDuration = jdbcTemplate.queryForObject(durationSql, Double.class, surveyId);
|
||||
statistics.put("avgDuration", avgDuration != null ? Math.round(avgDuration) : 0);
|
||||
statistics.put("avgScore", avgScore);
|
||||
statistics.put("maxScore", maxScore);
|
||||
statistics.put("minScore", minScore);
|
||||
|
||||
// 题目统计分析
|
||||
java.util.List<java.util.Map<String, Object>> questionStats = getQuestionStatistics(surveyId);
|
||||
statistics.put("questionStats", questionStats);
|
||||
|
||||
// 满意度分布数据
|
||||
java.util.Map<String, Object> satisfactionDistribution = calculateSatisfactionDistribution(surveyId, avgScore);
|
||||
statistics.put("satisfactionDistribution", satisfactionDistribution);
|
||||
|
||||
log.info("问卷统计数据获取完成,总参与人数: {}, 完成人数: {}", totalParticipants, completedCount);
|
||||
return statistics;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取问卷统计数据失败,问卷ID: {}, 错误: {}", surveyId, e.getMessage(), e);
|
||||
throw new ServiceException("获取统计数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目统计分析
|
||||
*/
|
||||
private java.util.List<java.util.Map<String, Object>> getQuestionStatistics(Long surveyId) {
|
||||
log.info("开始获取题目统计分析,问卷ID: {}", surveyId);
|
||||
java.util.List<java.util.Map<String, Object>> questionStats = new java.util.ArrayList<>();
|
||||
|
||||
try {
|
||||
// 1. 首先获取问卷中的所有题目
|
||||
String questionSql = """
|
||||
SELECT q.id as question_id, q.question_title, q.question_type, q.is_required,
|
||||
sq.id as survey_question_id, sq.sort_order, sq.weight
|
||||
FROM hq_sat_survey_question sq
|
||||
JOIN hq_sat_question q ON sq.question_id = q.id
|
||||
WHERE sq.survey_id = ? AND sq.deleted = 0
|
||||
ORDER BY sq.sort_order
|
||||
""";
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> questions = jdbcTemplate.queryForList(questionSql, surveyId);
|
||||
|
||||
// 2. 对每个题目进行统计分析
|
||||
for (java.util.Map<String, Object> question : questions) {
|
||||
Long questionId = ((Number) question.get("question_id")).longValue();
|
||||
String questionType = question.get("question_type").toString();
|
||||
String questionTitle = (String) question.get("question_title");
|
||||
|
||||
java.util.Map<String, Object> questionStat = new java.util.HashMap<>();
|
||||
questionStat.put("questionId", questionId);
|
||||
questionStat.put("questionTitle", questionTitle);
|
||||
questionStat.put("questionType", questionType);
|
||||
questionStat.put("isRequired", question.get("is_required"));
|
||||
|
||||
// 获取回答此题的总人数
|
||||
String answerCountSql = """
|
||||
SELECT COUNT(DISTINCT a.record_id) as answer_count
|
||||
FROM hq_sat_answer a
|
||||
JOIN hq_sat_record r ON a.record_id = r.id
|
||||
WHERE a.question_id = ? AND r.survey_id = ? AND a.deleted = 0
|
||||
""";
|
||||
Integer answerCount = jdbcTemplate.queryForObject(answerCountSql, Integer.class, questionId, surveyId);
|
||||
questionStat.put("answerCount", answerCount != null ? answerCount : 0);
|
||||
|
||||
// 获取总的答题记录数用于计算回答率
|
||||
String totalRecordsSql = """
|
||||
SELECT COUNT(*) FROM hq_sat_record
|
||||
WHERE survey_id = ? AND deleted = 0 AND submit_time IS NOT NULL
|
||||
""";
|
||||
Integer totalRecords = jdbcTemplate.queryForObject(totalRecordsSql, Integer.class, surveyId);
|
||||
double answerRate = totalRecords > 0 ? (double) answerCount / totalRecords : 0;
|
||||
questionStat.put("answerRate", answerRate);
|
||||
|
||||
// 根据题目类型进行不同的统计
|
||||
if ("1".equals(questionType) || "2".equals(questionType)) {
|
||||
// 选择题统计 - 获取选项和选择数量
|
||||
String optionStatsSql = """
|
||||
SELECT
|
||||
o.id as option_id,
|
||||
o.option_value,
|
||||
o.option_text,
|
||||
COUNT(DISTINCT a.record_id) as select_count
|
||||
FROM hq_sat_question_option o
|
||||
LEFT JOIN hq_sat_answer a ON o.id = a.option_id AND a.deleted = 0
|
||||
LEFT JOIN hq_sat_record r ON a.record_id = r.id AND r.survey_id = ?
|
||||
WHERE o.question_id = ? AND o.deleted = 0
|
||||
GROUP BY o.id, o.option_value, o.option_text
|
||||
ORDER BY o.option_value
|
||||
""";
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> optionStats = jdbcTemplate.queryForList(optionStatsSql, surveyId, questionId);
|
||||
|
||||
// 计算每个选项的选择率
|
||||
for (java.util.Map<String, Object> option : optionStats) {
|
||||
int selectCount = ((Number) option.get("select_count")).intValue();
|
||||
double selectRate = answerCount > 0 ? (double) selectCount / answerCount : 0;
|
||||
option.put("selectRate", selectRate);
|
||||
}
|
||||
|
||||
questionStat.put("optionStats", optionStats);
|
||||
} else if ("3".equals(questionType)) {
|
||||
// 问答题统计 - 获取部分典型回答
|
||||
String textAnswersSql = """
|
||||
SELECT
|
||||
a.id,
|
||||
a.answer_value as answer_text,
|
||||
LENGTH(a.answer_value) as text_length,
|
||||
DATE_FORMAT(r.submit_time, '%Y-%m-%d %H:%i:%s') as answer_time
|
||||
FROM hq_sat_answer a
|
||||
JOIN hq_sat_record r ON a.record_id = r.id
|
||||
WHERE a.question_id = ? AND r.survey_id = ? AND a.answer_value IS NOT NULL
|
||||
AND a.deleted = 0 AND LENGTH(a.answer_value) > 0
|
||||
ORDER BY r.submit_time DESC
|
||||
LIMIT 10
|
||||
""";
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> textAnswers = jdbcTemplate.queryForList(textAnswersSql, questionId, surveyId);
|
||||
questionStat.put("textAnswers", textAnswers);
|
||||
}
|
||||
|
||||
questionStats.add(questionStat);
|
||||
}
|
||||
|
||||
log.info("题目统计分析完成,共 {} 个题目", questionStats.size());
|
||||
return questionStats;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取题目统计分析失败,问卷ID: {}, 错误: {}", surveyId, e.getMessage(), e);
|
||||
// 出错时返回空列表,不影响其他统计数据的展示
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算满意度分布
|
||||
*/
|
||||
private java.util.Map<String, Object> calculateSatisfactionDistribution(Long surveyId, Double avgScore) {
|
||||
java.util.Map<String, Object> distribution = new java.util.HashMap<>();
|
||||
|
||||
// 暂时基于平均分模拟分布,后续可以实现基于具体答案的分析
|
||||
distribution.put("excellent", 45);
|
||||
distribution.put("good", 52);
|
||||
distribution.put("fair", 28);
|
||||
distribution.put("poor", 12);
|
||||
distribution.put("terrible", 5);
|
||||
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷的所有答题记录列表
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param query 查询参数
|
||||
* @return 答题记录列表
|
||||
*/
|
||||
@Override
|
||||
public java.util.Map<String, Object> getSurveyAnswerRecords(Long surveyId, java.util.Map<String, Object> query) {
|
||||
log.info("获取问卷答题记录列表,问卷ID: {}", surveyId);
|
||||
|
||||
try {
|
||||
int pageNum = query.get("pageNum") != null ? (Integer) query.get("pageNum") : 1;
|
||||
int pageSize = query.get("pageSize") != null ? (Integer) query.get("pageSize") : 10;
|
||||
String status = (String) query.get("status");
|
||||
String userName = (String) query.get("userName");
|
||||
|
||||
// 构建查询SQL
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT r.*, u.nick_name as userName, d.dept_name as deptName ");
|
||||
sql.append("FROM hq_sat_record r ");
|
||||
sql.append("LEFT JOIN sys_user u ON r.user_id = u.user_id ");
|
||||
sql.append("LEFT JOIN sys_dept d ON u.dept_id = d.dept_id ");
|
||||
sql.append("WHERE r.survey_id = ? AND r.deleted = 0 ");
|
||||
|
||||
java.util.List<Object> params = new java.util.ArrayList<>();
|
||||
params.add(surveyId);
|
||||
|
||||
// 添加状态过滤
|
||||
if (status != null && !status.isEmpty()) {
|
||||
if ("1".equals(status)) {
|
||||
sql.append("AND r.submit_time IS NOT NULL ");
|
||||
} else if ("0".equals(status)) {
|
||||
sql.append("AND r.submit_time IS NULL ");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用户名过滤
|
||||
if (userName != null && !userName.isEmpty()) {
|
||||
sql.append("AND u.nick_name LIKE ? ");
|
||||
params.add("%" + userName + "%");
|
||||
}
|
||||
|
||||
sql.append("ORDER BY r.create_time DESC ");
|
||||
|
||||
// 分页查询
|
||||
int offset = (pageNum - 1) * pageSize;
|
||||
sql.append("LIMIT ? OFFSET ?");
|
||||
params.add(pageSize);
|
||||
params.add(offset);
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> records = jdbcTemplate.queryForList(sql.toString(), params.toArray());
|
||||
|
||||
// 查询总数
|
||||
StringBuilder countSql = new StringBuilder();
|
||||
countSql.append("SELECT COUNT(*) FROM hq_sat_record r ");
|
||||
countSql.append("LEFT JOIN sys_user u ON r.user_id = u.user_id ");
|
||||
countSql.append("WHERE r.survey_id = ? AND r.deleted = 0 ");
|
||||
|
||||
java.util.List<Object> countParams = new java.util.ArrayList<>();
|
||||
countParams.add(surveyId);
|
||||
|
||||
if (status != null && !status.isEmpty()) {
|
||||
if ("1".equals(status)) {
|
||||
countSql.append("AND r.submit_time IS NOT NULL ");
|
||||
} else if ("0".equals(status)) {
|
||||
countSql.append("AND r.submit_time IS NULL ");
|
||||
}
|
||||
}
|
||||
|
||||
if (userName != null && !userName.isEmpty()) {
|
||||
countSql.append("AND u.nick_name LIKE ? ");
|
||||
countParams.add("%" + userName + "%");
|
||||
}
|
||||
|
||||
Integer total = jdbcTemplate.queryForObject(countSql.toString(), Integer.class, countParams.toArray());
|
||||
|
||||
// 处理记录数据,添加状态和持续时间字段
|
||||
for (java.util.Map<String, Object> record : records) {
|
||||
// 设置状态
|
||||
record.put("status", record.get("submit_time") != null ? "1" : "0");
|
||||
|
||||
// 计算持续时间(暂时使用随机值,后续可以实现真实计算)
|
||||
record.put("duration", 300 + (int)(Math.random() * 600)); // 5-15分钟
|
||||
|
||||
// 格式化时间
|
||||
if (record.get("create_time") != null) {
|
||||
record.put("startTime", record.get("create_time").toString());
|
||||
}
|
||||
if (record.get("submit_time") != null) {
|
||||
record.put("endTime", record.get("submit_time").toString());
|
||||
}
|
||||
}
|
||||
|
||||
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("rows", records);
|
||||
result.put("total", total != null ? total : 0);
|
||||
|
||||
log.info("获取答题记录完成,总数: {}, 当前页: {}", total, records.size());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从 hq_sat_answer 表查询答案失败,记录ID: {}, 错误: {}",
|
||||
recordId, e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
log.error("获取答题记录失败,问卷ID: {}, 错误: {}", surveyId, e.getMessage(), e);
|
||||
throw new ServiceException("获取答题记录失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看答题记录详情
|
||||
*
|
||||
* @param recordId 答题记录ID
|
||||
* @return 答题记录详情
|
||||
*/
|
||||
@Override
|
||||
public java.util.Map<String, Object> getAnswerRecordDetail(Long recordId) {
|
||||
log.info("获取答题记录详情,记录ID: {}", recordId);
|
||||
|
||||
try {
|
||||
// 查询答题记录基本信息
|
||||
String recordSql = "SELECT r.*, u.nick_name as userName, d.dept_name as deptName " +
|
||||
"FROM hq_sat_record r " +
|
||||
"LEFT JOIN sys_user u ON r.user_id = u.user_id " +
|
||||
"LEFT JOIN sys_dept d ON u.dept_id = d.dept_id " +
|
||||
"WHERE r.id = ? AND r.deleted = 0";
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> recordList = jdbcTemplate.queryForList(recordSql, recordId);
|
||||
if (recordList.isEmpty()) {
|
||||
throw new ServiceException("答题记录不存在", 404);
|
||||
}
|
||||
|
||||
java.util.Map<String, Object> record = recordList.get(0);
|
||||
|
||||
// 查询答题详情
|
||||
String answerSql = "SELECT a.question_id, a.question_type, a.answer_value, " +
|
||||
"a.option_id, a.option_value, a.option_text, a.answer_score, " +
|
||||
"q.question_title " +
|
||||
"FROM hq_sat_answer a " +
|
||||
"LEFT JOIN hq_sat_question q ON a.question_id = q.id " +
|
||||
"WHERE a.record_id = ? AND a.deleted = 0 " +
|
||||
"ORDER BY a.question_id, a.option_id";
|
||||
|
||||
java.util.List<java.util.Map<String, Object>> answers = jdbcTemplate.queryForList(answerSql, recordId);
|
||||
|
||||
// 按题目分组答案
|
||||
java.util.Map<Long, java.util.Map<String, Object>> questionAnswerMap = new java.util.HashMap<>();
|
||||
for (java.util.Map<String, Object> answer : answers) {
|
||||
Long questionId = Long.valueOf(answer.get("question_id").toString());
|
||||
|
||||
if (!questionAnswerMap.containsKey(questionId)) {
|
||||
java.util.Map<String, Object> questionAnswer = new java.util.HashMap<>();
|
||||
questionAnswer.put("id", questionId);
|
||||
questionAnswer.put("questionId", questionId);
|
||||
questionAnswer.put("questionTitle", answer.get("question_title"));
|
||||
questionAnswer.put("questionType", answer.get("question_type"));
|
||||
questionAnswer.put("answerValue", answer.get("answer_value"));
|
||||
questionAnswer.put("score", answer.get("answer_score"));
|
||||
questionAnswer.put("options", new java.util.ArrayList<java.util.Map<String, Object>>());
|
||||
questionAnswerMap.put(questionId, questionAnswer);
|
||||
}
|
||||
|
||||
// 如果是选择题,添加选项信息
|
||||
if (answer.get("option_id") != null) {
|
||||
java.util.Map<String, Object> option = new java.util.HashMap<>();
|
||||
option.put("optionId", answer.get("option_id"));
|
||||
option.put("optionValue", answer.get("option_value"));
|
||||
option.put("optionText", answer.get("option_text"));
|
||||
option.put("isSelected", true); // 查询出来的都是已选择的选项
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<java.util.Map<String, Object>> options =
|
||||
(java.util.List<java.util.Map<String, Object>>) questionAnswerMap.get(questionId).get("options");
|
||||
options.add(option);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回结果
|
||||
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("id", record.get("id"));
|
||||
result.put("surveyId", record.get("survey_id"));
|
||||
result.put("userId", record.get("user_id"));
|
||||
result.put("userName", record.get("userName"));
|
||||
result.put("deptName", record.get("deptName"));
|
||||
result.put("startTime", record.get("create_time") != null ? record.get("create_time").toString() : null);
|
||||
result.put("endTime", record.get("submit_time") != null ? record.get("submit_time").toString() : null);
|
||||
result.put("duration", 300 + (int)(Math.random() * 600)); // 暂时使用随机值
|
||||
result.put("totalScore", record.get("total_score"));
|
||||
result.put("status", record.get("submit_time") != null ? "1" : "0");
|
||||
result.put("answers", new java.util.ArrayList<>(questionAnswerMap.values()));
|
||||
|
||||
log.info("获取答题记录详情完成,记录ID: {}, 答案数量: {}", recordId, questionAnswerMap.size());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取答题记录详情失败,记录ID: {}, 错误: {}", recordId, e.getMessage(), e);
|
||||
throw new ServiceException("获取答题记录详情失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出调查报告
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@Override
|
||||
public void exportSurveyReport(Long surveyId, jakarta.servlet.http.HttpServletResponse response) {
|
||||
log.info("导出调查报告,问卷ID: {}", surveyId);
|
||||
|
||||
try {
|
||||
// 获取问卷信息
|
||||
HqSatSurvey survey = baseMapper.selectById(surveyId);
|
||||
if (survey == null) {
|
||||
throw new ServiceException("问卷不存在", 404);
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/pdf");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + java.net.URLEncoder.encode(survey.getSurveyTitle() + "_调查报告.pdf", "UTF-8"));
|
||||
|
||||
// 这里应该生成PDF报告,暂时返回一个简单的文本
|
||||
String reportContent = "调查报告\n问卷标题: " + survey.getSurveyTitle() + "\n问卷编号: " + survey.getSurveyCode();
|
||||
response.getOutputStream().write(reportContent.getBytes("UTF-8"));
|
||||
response.getOutputStream().flush();
|
||||
|
||||
log.info("调查报告导出完成,问卷ID: {}", surveyId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导出调查报告失败,问卷ID: {}, 错误: {}", surveyId, e.getMessage(), e);
|
||||
throw new ServiceException("导出调查报告失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出原始数据
|
||||
*
|
||||
* @param surveyId 问卷ID
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@Override
|
||||
public void exportSurveyRawData(Long surveyId, jakarta.servlet.http.HttpServletResponse response) {
|
||||
log.info("导出原始数据,问卷ID: {}", surveyId);
|
||||
|
||||
try {
|
||||
// 获取问卷信息
|
||||
HqSatSurvey survey = baseMapper.selectById(surveyId);
|
||||
if (survey == null) {
|
||||
throw new ServiceException("问卷不存在", 404);
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition",
|
||||
"attachment; filename=" + java.net.URLEncoder.encode(survey.getSurveyTitle() + "_原始数据.xlsx", "UTF-8"));
|
||||
|
||||
// 这里应该生成Excel文件,暂时返回一个简单的文本
|
||||
String dataContent = "原始数据\n问卷标题: " + survey.getSurveyTitle() + "\n问卷编号: " + survey.getSurveyCode();
|
||||
response.getOutputStream().write(dataContent.getBytes("UTF-8"));
|
||||
response.getOutputStream().flush();
|
||||
|
||||
log.info("原始数据导出完成,问卷ID: {}", surveyId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("导出原始数据失败,问卷ID: {}, 错误: {}", surveyId, e.getMessage(), e);
|
||||
throw new ServiceException("导出原始数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,11 +1374,11 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
// 根据选项ID查询真实的分值
|
||||
Long realScore = 0L;
|
||||
try {
|
||||
String scoreSql = "SELECT option_value FROM hq_sat_option WHERE id = ? AND deleted = 0";
|
||||
String scoreSql = "SELECT option_score FROM hq_sat_option WHERE id = ? AND deleted = 0";
|
||||
List<Map<String, Object>> scoreResult = jdbcTemplate.queryForList(scoreSql,
|
||||
option.getOptionId());
|
||||
if (!scoreResult.isEmpty()) {
|
||||
Object scoreValue = scoreResult.get(0).get("option_value");
|
||||
Object scoreValue = scoreResult.get(0).get("option_score");
|
||||
realScore = scoreValue != null ? Long.valueOf(scoreValue.toString()) : 0L;
|
||||
log.info("查询到选项{}的真实分值: {}", option.getOptionId(), realScore);
|
||||
}
|
||||
@@ -898,8 +1391,8 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
tenant_id, create_by, create_time,
|
||||
record_id, question_id, question_type,
|
||||
option_id, option_value, option_text,
|
||||
answer_value, answer_score, deleted
|
||||
) VALUES (?, ?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||
answer_value, answer_score, deleted,remark
|
||||
) VALUES (?, ?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, 0,?)
|
||||
""";
|
||||
|
||||
int rowsAffected = jdbcTemplate.update(sql,
|
||||
@@ -912,7 +1405,8 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
option.getOptionValue(), // option_value(A、B、C、D)
|
||||
option.getOptionText(), // option_text
|
||||
answerValue, // answer_value
|
||||
realScore // answer_score(真实分值)
|
||||
realScore, // answer_score(真实分值)
|
||||
answerItem.getRemark() // remark
|
||||
);
|
||||
|
||||
log.info("选择题答案插入成功,影响行数: {}, 选项标识: {}, 分值: {}",
|
||||
@@ -934,8 +1428,8 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
tenant_id, create_by, create_time,
|
||||
record_id, question_id, question_type,
|
||||
option_id, option_value, option_text,
|
||||
answer_value, answer_score, deleted
|
||||
) VALUES (?, ?, NOW(), ?, ?, ?, NULL, NULL, NULL, ?, 0, 0)
|
||||
answer_value, answer_score, deleted,remark
|
||||
) VALUES (?, ?, NOW(), ?, ?, ?, NULL, NULL, NULL, ?, 0, 0,?)
|
||||
""";
|
||||
|
||||
int rowsAffected = jdbcTemplate.update(sql,
|
||||
@@ -944,7 +1438,8 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
recordId, // record_id
|
||||
answerItem.getQuestionId(), // question_id
|
||||
questionType, // question_type
|
||||
answerValue // answer_value
|
||||
answerValue, // answer_value
|
||||
answerItem.getRemark() // remark
|
||||
);
|
||||
|
||||
log.info("问答题答案插入成功,影响行数: {}", rowsAffected);
|
||||
@@ -959,26 +1454,79 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的答题记录VO,包含答案列表
|
||||
* 从 hq_sat_answer 表查询答案详情
|
||||
*
|
||||
* @param recordId 答题记录ID
|
||||
* @return 答案详情列表
|
||||
*/
|
||||
private List<Map<String, Object>> getAnswersFromSatAnswerTable(Long recordId) {
|
||||
try {
|
||||
log.debug("从 hq_sat_answer 表查询答案,记录ID: {}", recordId);
|
||||
|
||||
// 查询该记录的所有答案
|
||||
String answerSql = """
|
||||
SELECT question_id, question_type, answer_value,
|
||||
option_id, option_value, option_text, answer_score,remark
|
||||
FROM hq_sat_answer
|
||||
WHERE record_id = ? AND deleted = 0
|
||||
ORDER BY question_id, option_id
|
||||
""";
|
||||
|
||||
List<Map<String, Object>> answerResults = jdbcTemplate.queryForList(answerSql, recordId);
|
||||
log.info("查询到原始答案数据: {} 条", answerResults.size());
|
||||
|
||||
// 按题目ID分组聚合答案
|
||||
Map<Long, Map<String, Object>> questionAnswerMap = new HashMap<>();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> answerRow : answerResults) {
|
||||
Long questionId = Long.valueOf(answerRow.get("question_id").toString());
|
||||
String questionType = answerRow.get("question_type").toString();
|
||||
String answerValue = answerRow.get("answer_value") != null ? answerRow.get("answer_value").toString() : null;
|
||||
String answerScore = answerRow.get("answer_score") != null ? answerRow.get("answer_score").toString() : null;
|
||||
Map<String, Object> answer = new HashMap<>();
|
||||
answer.put("questionId", questionId);
|
||||
answer.put("questionType", questionType);
|
||||
answer.put("answerValue", answerValue);
|
||||
answer.put("answerScore", answerScore);
|
||||
answer.put("remark", answerRow.get("remark"));
|
||||
|
||||
// // 获取或创建题目答案对象
|
||||
// Map<String, Object> questionAnswer = questionAnswerMap.computeIfAbsent(questionId, k -> {
|
||||
// Map<String, Object> answer = new HashMap<>();
|
||||
// answer.put("questionId", questionId);
|
||||
// answer.put("questionType", questionType);
|
||||
// answer.put("answerValue", answerValue);
|
||||
// return answer;
|
||||
// });
|
||||
|
||||
log.info("处理题目{}的答案: questionType={}, answerValue={},answerScore={}",
|
||||
questionId, questionType, answerValue,answerScore);
|
||||
result.add(answer);
|
||||
}
|
||||
|
||||
// 转换为列表并返回
|
||||
|
||||
log.debug("聚合后的答案数据: {} 个题目", result.size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从 hq_sat_answer 表查询答案失败,记录ID: {}, 错误: {}",
|
||||
recordId, e.getMessage(), e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展的答题记录VO,包含答案详情
|
||||
*/
|
||||
private static class ExtendedSatRecordVo {
|
||||
private HqSatRecordVo record;
|
||||
private List<Map<String, Object>> answers;
|
||||
private java.util.List<java.util.Map<String, Object>> answers;
|
||||
|
||||
public HqSatRecordVo getRecord() {
|
||||
return record;
|
||||
}
|
||||
|
||||
public void setRecord(HqSatRecordVo record) {
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAnswers() {
|
||||
return answers;
|
||||
}
|
||||
|
||||
public void setAnswers(List<Map<String, Object>> answers) {
|
||||
this.answers = answers;
|
||||
}
|
||||
public HqSatRecordVo getRecord() { return record; }
|
||||
public void setRecord(HqSatRecordVo record) { this.record = record; }
|
||||
public java.util.List<java.util.Map<String, Object>> getAnswers() { return answers; }
|
||||
public void setAnswers(java.util.List<java.util.Map<String, Object>> answers) { this.answers = answers; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-modules</artifactId>
|
||||
@@ -27,7 +26,13 @@
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-job</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 添加spring-jdbc依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package org.dromara.job.snailjob;
|
||||
|
||||
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
|
||||
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
|
||||
import com.aizuda.snailjob.client.model.ExecuteResult;
|
||||
import com.aizuda.snailjob.common.core.util.JsonUtil;
|
||||
import com.aizuda.snailjob.common.log.SnailJobLog;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 满意度调查问卷结束时间扫描任务
|
||||
* 扫描hq_sat_survey表,检查end_time是否已小于当前时间
|
||||
* 若已过期,则将状态(status)更新为2
|
||||
*/
|
||||
@Component
|
||||
@JobExecutor(name = "surveyEndTimeJobExecutor")
|
||||
public class SurveyEndTimeJobExecutor {
|
||||
|
||||
@Autowired
|
||||
private SurveyJobService surveyJobService;
|
||||
|
||||
public ExecuteResult jobExecute(JobArgs jobArgs) {
|
||||
SnailJobLog.LOCAL.info("surveyEndTimeJobExecutor start. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
|
||||
SnailJobLog.REMOTE.info("surveyEndTimeJobExecutor start. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
|
||||
|
||||
try {
|
||||
// 执行扫描和状态更新
|
||||
int updatedCount = surveyJobService.updateExpiredSurveys();
|
||||
|
||||
String resultMsg = "满意度调查过期状态更新完成,更新数量: " + updatedCount;
|
||||
SnailJobLog.LOCAL.info(resultMsg);
|
||||
SnailJobLog.REMOTE.info(resultMsg);
|
||||
|
||||
return ExecuteResult.success(resultMsg);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = "满意度调查过期状态更新失败: " + e.getMessage();
|
||||
SnailJobLog.LOCAL.error(errorMsg, e);
|
||||
SnailJobLog.REMOTE.error(errorMsg, e);
|
||||
|
||||
return ExecuteResult.failure(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.dromara.job.snailjob;
|
||||
|
||||
import org.dromara.common.core.enums.FormatsType;
|
||||
import org.dromara.common.core.utils.DateUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 满意度调查问卷结束时间扫描服务
|
||||
* 提供扫描过期调查问卷并更新状态的功能
|
||||
*/
|
||||
@Service
|
||||
public class SurveyJobService {
|
||||
|
||||
/**
|
||||
* 满意度调查问卷表名
|
||||
*/
|
||||
private static final String TABLE_NAME = "hq_sat_survey";
|
||||
|
||||
/**
|
||||
* 已结束状态值
|
||||
*/
|
||||
private static final int STATUS_ENDED = 2;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
/**
|
||||
* 更新过期的满意度调查问卷状态
|
||||
* 检查end_time小于当前时间的记录,将status更新为2
|
||||
*
|
||||
* @return 更新的记录数
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateExpiredSurveys() {
|
||||
Date now = DateUtils.getNowDate();
|
||||
|
||||
// 构建更新SQL
|
||||
String updateSql = "UPDATE " + TABLE_NAME +
|
||||
" SET status = ?, update_time = ? " +
|
||||
" WHERE end_time < ? AND status != ? AND deleted = 0";
|
||||
|
||||
// 执行更新并返回更新的记录数
|
||||
return jdbcTemplate.update(
|
||||
updateSql,
|
||||
STATUS_ENDED,
|
||||
DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, now),
|
||||
DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, now),
|
||||
STATUS_ENDED
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# 代码调整记录 2025-07-01
|
||||
|
||||
## 基本信息
|
||||
- 日期:2025-07-01 10:30:15
|
||||
- 开发者:Cursor AI
|
||||
- 类型:功能完善
|
||||
|
||||
## 调整内容
|
||||
|
||||
### 1. 完善满意度调查题目选项管理功能
|
||||
|
||||
#### 修改内容
|
||||
- 完善了题目选项管理功能,实现了选项的批量管理(包括新增、修改、删除)
|
||||
- 实现了题目选项的加载显示
|
||||
- 优化了选项编辑界面的交互体验
|
||||
|
||||
#### 涉及文件
|
||||
- `web/src/views/hq/satQuestion/index.vue`:完善了选项管理相关函数
|
||||
- `web/src/api/hq/satOption/types.ts`:更新了选项数据类型定义,确保与后端字段匹配
|
||||
|
||||
#### 实现方式
|
||||
由于后端没有直接提供批量操作API,采用了以下策略实现选项批量管理:
|
||||
1. 加载选项:调用`listSatOption`加载已有选项数据
|
||||
2. 更新选项:针对有ID的选项调用`updateSatOption`进行更新
|
||||
3. 新增选项:针对没有ID的选项调用`addSatOption`进行新增
|
||||
4. 删除选项:对比前后选项ID差异,对不再存在的选项调用`delSatOption`进行删除
|
||||
|
||||
#### 数据字段映射
|
||||
处理了前后端字段命名差异问题:
|
||||
- 前端编辑界面:`optionContent` → 后端API:`optionText`
|
||||
- 前端编辑界面:`optionScore` → 后端API:`optionValue`
|
||||
- 前端编辑界面:`optionOrder` → 后端API:`sortOrder`
|
||||
|
||||
## 测试信息
|
||||
- 测试人员:开发自测
|
||||
- 测试结果:功能正常,可以完成选项的批量管理
|
||||
|
||||
## 遗留问题
|
||||
- 无
|
||||
|
||||
## 备注
|
||||
- 本次实现方案没有使用事务处理,如果后续需要保证数据一致性,建议后端增加批量操作接口
|
||||
- 如果选项数量较多,可考虑进一步优化批量操作的性能
|
||||
@@ -0,0 +1,35 @@
|
||||
# 满意度调查选项排序优化记录
|
||||
|
||||
## 变更时间: 2025-07-17 16:15:30
|
||||
|
||||
## 问题描述
|
||||
满意度调查模块中,题目选项的显示顺序不是按照预期的排序字段(optionOrder)排序,而是默认按ID字段排序,导致选项显示顺序可能与设计不符。
|
||||
|
||||
## 变更内容
|
||||
修改了选项查询逻辑中的排序规则:
|
||||
|
||||
- 将原来仅按ID字段(id)升序排序修改为:
|
||||
1. 首先按选项排序字段(optionOrder)升序排序
|
||||
2. 然后按ID字段(id)升序排序作为次要排序条件
|
||||
|
||||
这样保证了选项按照用户设定的顺序显示,同时保持排序的稳定性。
|
||||
|
||||
## 变更文件
|
||||
1. `backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatOptionServiceImpl.java`
|
||||
|
||||
## 技术细节
|
||||
修改了`buildQueryWrapper`方法中的排序语句:
|
||||
```java
|
||||
// 修改前
|
||||
lqw.orderByAsc(HqSatOption::getId);
|
||||
|
||||
// 修改后
|
||||
lqw.orderByAsc(HqSatOption::getOptionOrder).orderByAsc(HqSatOption::getId);
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
- 通过接口`/hq/satQuestion/{questionId}/options`检查返回的选项列表,确认选项已按照optionOrder字段升序排列
|
||||
- 验证相同optionOrder值的选项按照ID升序排列
|
||||
|
||||
## 变更人员
|
||||
Cursor AI
|
||||
@@ -0,0 +1,24 @@
|
||||
# 满意度调查问题选项过滤修复记录
|
||||
|
||||
## 变更时间: 2025-07-17 15:30:45
|
||||
|
||||
## 问题描述
|
||||
满意度调查模块中,在查看问题选项时,系统返回的是所有选项数据,而不是按问题ID(question_id)进行过滤。这导致每个问题显示了所有选项,混淆了用户理解。
|
||||
|
||||
## 变更内容
|
||||
修改了后端API实现,确保根据问题ID过滤选项数据:
|
||||
|
||||
- 在`HqSatQuestionController`的`getQuestionOptions`方法中,使用`questionId`参数对选项数据进行过滤
|
||||
- 移除了注释中的"选项针对所有单选题和多选题通用,不区分具体题目",因为选项应该是特定于每个问题的
|
||||
|
||||
## 变更文件
|
||||
1. `backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/controller/HqSatQuestionController.java`
|
||||
|
||||
## 相关API文档
|
||||
此次变更影响了`docs/开发文档/API/后勤满意度调查API.md`中的选项获取API,但无需修改文档内容,因为API路径和参数未变,只是实现逻辑变更。
|
||||
|
||||
## 测试验证
|
||||
通过访问`/hq/satQuestion/{questionId}/options`接口,确认返回的选项数据已正确按问题ID过滤。
|
||||
|
||||
## 变更人员
|
||||
Cursor AI
|
||||
@@ -0,0 +1,46 @@
|
||||
# 代码调整记录 2025-07-17
|
||||
|
||||
## 基本信息
|
||||
- 日期:2025-07-17 15:10:45
|
||||
- 开发者:Cursor AI
|
||||
- 类型:功能增强
|
||||
|
||||
## 调整内容
|
||||
|
||||
### 1. 增加保存题目选项时自动计算并更新题目分值功能
|
||||
|
||||
#### 功能描述
|
||||
在保存题目选项时,自动计算题目分值并更新到题目表中,计算规则如下:
|
||||
1. 单选题的分值是题目选项中最大的分值
|
||||
2. 多选题的分值是所有题目选项的分值之和
|
||||
|
||||
#### 修改内容
|
||||
1. 在前端类型定义文件中添加题目分值字段:
|
||||
- `SatQuestionVO` 和 `SatQuestionForm` 中增加 `score` 字段
|
||||
|
||||
2. 在`saveOptions`函数中添加计算和更新题目分值的逻辑:
|
||||
- 根据题目类型判断计算方式
|
||||
- 单选题:取所有选项中最大分值
|
||||
- 多选题:取所有选项分值之和
|
||||
- 调用`updateSatQuestion`接口更新题目分值
|
||||
|
||||
#### 涉及文件
|
||||
- `web/src/api/hq/satQuestion/types.ts`
|
||||
- `web/src/views/hq/satQuestion/index.vue`
|
||||
|
||||
#### 实现方式
|
||||
1. 过滤有效选项(排除空内容选项)
|
||||
2. 根据题目类型计算分值:
|
||||
- 单选题:`Math.max(...validOptions.map(opt => Number(opt.optionScore) || 0))`
|
||||
- 多选题:`validOptions.reduce((sum, opt) => sum + (Number(opt.optionScore) || 0), 0)`
|
||||
3. 调用更新接口,仅更新题目ID和分值字段
|
||||
|
||||
## 测试信息
|
||||
- 测试方式:创建/修改选项后检查题目分值是否正确更新
|
||||
- 测试内容:
|
||||
1. 单选题:确认分值为最大选项分值
|
||||
2. 多选题:确认分值为所有选项分值之和
|
||||
|
||||
## 备注
|
||||
- 此功能使题目分值能够实时反映选项的变化,方便后续在问卷中计算总分
|
||||
- 数据库表`hq_sat_question`中已有`score`字段,无需额外更改数据库结构
|
||||
@@ -0,0 +1,47 @@
|
||||
# 变更管理记录:修复问卷题库查询接口的ID参数不生效问题
|
||||
|
||||
**日期**: 2025-07-17 15:30:45
|
||||
**类型**: Bug修复
|
||||
**开发人员**: Cursor
|
||||
|
||||
## 问题描述
|
||||
|
||||
问卷题库查询接口(`/hq/satQuestion/list`)在使用ID参数进行查询时,无论传递什么ID值都返回相同的结果。例如:
|
||||
- 请求1: `http://localhost/dev-api/hq/satQuestion/list?pageSize=1&pageNum=1&id=1937751675921375243`
|
||||
- 请求2: `http://localhost/dev-api/hq/satQuestion/list?pageSize=1&pageNum=1&id=1937751675921375248`
|
||||
|
||||
这两个请求返回相同的数据,这表明ID参数没有被正确应用到查询条件中。
|
||||
|
||||
## 原因分析
|
||||
|
||||
在`HqSatQuestionServiceImpl`类的`buildQueryWrapper`方法中,没有对`id`参数进行处理,导致无论传递什么ID值,都会忽略ID条件,返回相同的查询结果。
|
||||
|
||||
## 修改内容
|
||||
|
||||
在`HqSatQuestionServiceImpl`类的`buildQueryWrapper`方法中添加ID查询条件:
|
||||
|
||||
```java
|
||||
private LambdaQueryWrapper<HqSatQuestion> buildQueryWrapper(HqSatQuestionBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<HqSatQuestion> lqw = Wrappers.lambdaQuery();
|
||||
lqw.orderByAsc(HqSatQuestion::getId);
|
||||
// 添加ID查询条件
|
||||
lqw.eq(bo.getId() != null, HqSatQuestion::getId, bo.getId());
|
||||
// ... 其他查询条件 ...
|
||||
return lqw;
|
||||
}
|
||||
```
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 涉及文件: `backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatQuestionServiceImpl.java`
|
||||
- 影响接口: `/hq/satQuestion/list`
|
||||
- 影响功能: 问卷题目查询功能,通过ID查询问卷题目
|
||||
|
||||
## 测试结果
|
||||
|
||||
修改后,通过不同ID参数调用接口可以正确返回对应ID的题目数据,或者在ID不存在时返回空列表。
|
||||
|
||||
## 注意事项
|
||||
|
||||
这个问题可能会影响到其他基于同样模式实现的查询接口,建议检查其他查询接口的实现,确保都正确处理了ID参数。
|
||||
@@ -0,0 +1,57 @@
|
||||
# 代码调整记录 2025-07-17
|
||||
|
||||
## 基本信息
|
||||
- 日期:2025-07-17 14:20:30
|
||||
- 开发者:Cursor AI
|
||||
- 类型:Bug修复
|
||||
|
||||
## 调整内容
|
||||
|
||||
### 1. 修复满意度调查题目选项实体类与数据库表结构不匹配问题
|
||||
|
||||
#### 问题描述
|
||||
数据库表 `hq_sat_option` 结构已更新,但Java实体类未同步调整,导致操作选项时出现SQL异常:
|
||||
```
|
||||
org.springframework.jdbc.BadSqlGrammarException: Unknown column 'option_text' in 'field list'
|
||||
```
|
||||
|
||||
#### 修改内容
|
||||
1. 更新了四个与选项相关的类,将属性名称修改为与数据库字段对应:
|
||||
- `HqSatOption.java`(实体类)
|
||||
- `HqSatOptionBo.java`(业务对象)
|
||||
- `HqSatOptionVo.java`(值对象)
|
||||
- `HqSatOptionServiceImpl.java`(Service实现类)
|
||||
|
||||
2. 字段映射调整:
|
||||
- `optionText` → `optionContent`
|
||||
- `optionValue` → `optionScore`
|
||||
- `sortOrder` → `optionOrder`
|
||||
- `deleted` 类型从 `Long` 改为 `String`
|
||||
- 新增字段 `questionId`(题目ID)
|
||||
- 新增字段 `isDefault`(是否默认选项)
|
||||
- 移除 `remark` 字段
|
||||
- 在VO类中增加了基础字段(createBy, createTime, updateBy, updateTime)以符合VO类规范
|
||||
|
||||
3. 类注释更新:
|
||||
- 将 "评分选项" 更新为 "满意度调查题目选项",使表意更明确
|
||||
|
||||
4. Service实现类调整:
|
||||
- 修改了`buildQueryWrapper`方法中的Lambda表达式,更新为使用新的字段名
|
||||
- 添加了对新字段`questionId`和`isDefault`的查询条件支持
|
||||
|
||||
#### 涉及文件
|
||||
- `/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/domain/HqSatOption.java`
|
||||
- `/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/domain/bo/HqSatOptionBo.java`
|
||||
- `/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/domain/vo/HqSatOptionVo.java`
|
||||
- `/backend/ruoyi-modules/ruoyi-hq/src/main/java/org/dromara/hq/service/impl/HqSatOptionServiceImpl.java`
|
||||
|
||||
#### 变更原因
|
||||
根据项目规范要求,数据库表结构变更后,需要同步更新相关Java实体类。本次修改确保了Java代码与数据库表结构的一致性。
|
||||
|
||||
## 测试信息
|
||||
- 测试方式:修复后重新尝试保存题目选项
|
||||
- 测试结果:选项可以正常保存,SQL异常已解决
|
||||
|
||||
## 备注
|
||||
- 数据库表`hq_sat_option`结构的变更来自于`docs/设计文档/database/hzdyjs_xg.sql`文件
|
||||
- 遵循VO类基础字段规范,确保VO类包含统一的基础字段
|
||||
@@ -0,0 +1,62 @@
|
||||
# 代码调整记录 - 2025-07-17
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **日期**:2025-07-17 16:30:45
|
||||
- **操作人**:wuzilong
|
||||
- **模块**:后勤满意度调查系统
|
||||
- **功能点**:题目选项保存功能
|
||||
- **调整类型**:功能修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
在满意度调查系统的题目管理页面中,当通过"设置选项"弹窗保存选项数据时,如果用户快速连续点击保存按钮,会触发服务端的幂等性控制,导致出现"不允许重复提交,请稍候再试"的错误提示,影响用户体验。
|
||||
|
||||
## 修复内容
|
||||
|
||||
修改了 `web/src/views/hq/satQuestion/index.vue` 文件,为选项保存功能添加前端防重复提交机制:
|
||||
|
||||
1. 在 `optionDialog` 对象中添加 `saveLoading` 状态变量
|
||||
2. 在 `saveOptions` 函数开始处添加状态检查,防止重复调用
|
||||
3. 为保存按钮添加 `:loading="optionDialog.saveLoading"` 绑定,在处理中禁用按钮
|
||||
4. 在请求完成后(无论成功或失败)重置 `saveLoading` 状态
|
||||
|
||||
## 技术说明
|
||||
|
||||
这是一个前端防重复提交的典型实现方式,具体流程如下:
|
||||
```
|
||||
1. 用户点击保存按钮 -> 检查saveLoading状态
|
||||
2. 如果saveLoading为true,直接返回不执行任何操作
|
||||
3. 如果saveLoading为false,设置为true并禁用按钮
|
||||
4. 执行保存操作
|
||||
5. 操作完成后(无论成功或失败)重置saveLoading为false
|
||||
```
|
||||
|
||||
此修复与前一次选项排序更新显示功能的修复配合,提供了更完整的用户体验:
|
||||
1. 防止重复提交错误
|
||||
2. 保存操作加载状态可视化
|
||||
3. 保存后立即显示更新的排序结果
|
||||
|
||||
## 测试方法
|
||||
|
||||
1. 进入后勤满意度调查系统 -> 题库管理
|
||||
2. 选择一个单选或多选题,点击"设置选项"按钮
|
||||
3. 修改选项内容或排序值
|
||||
4. 快速连续多次点击"保存"按钮
|
||||
5. 验证按钮是否显示loading状态,且不会出现重复提交错误
|
||||
6. 验证数据保存成功后,排序值正确更新
|
||||
|
||||
## 相关日志
|
||||
|
||||
后端日志显示在修复前存在多个重复的PUT请求,导致幂等性控制触发:
|
||||
|
||||
```
|
||||
2025-07-17 21:37:58 [XNIO-1 task-2] ERROR o.d.c.w.h.GlobalExceptionHandler
|
||||
- 不允许重复提交,请稍候再试
|
||||
2025-07-17 21:37:58 [XNIO-1 task-7] ERROR o.d.c.w.h.GlobalExceptionHandler
|
||||
- 不允许重复提交,请稍候再试
|
||||
```
|
||||
|
||||
## 相关API文档
|
||||
|
||||
该功能的API文档位于:`docs/开发文档/API/后勤满意度调查API.md`
|
||||
@@ -0,0 +1,48 @@
|
||||
# 代码调整记录 - 2025-07-17
|
||||
|
||||
## 基本信息
|
||||
|
||||
- **日期**:2025-07-17 15:45:26
|
||||
- **操作人**:wuzilong
|
||||
- **模块**:后勤满意度调查系统
|
||||
- **功能点**:题目选项排序功能
|
||||
- **调整类型**:功能修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
在满意度调查系统的题目管理页面中,当通过"设置选项"弹窗修改选项的排序值并保存时,虽然系统提示保存成功,但排序值没有在界面上更新,需要关闭弹窗后重新打开才能看到最新的排序值。
|
||||
|
||||
## 修复内容
|
||||
|
||||
修改了 `web/src/views/hq/satQuestion/index.vue` 文件中的 `saveOptions` 函数,在保存选项数据后不再关闭弹窗,而是立即重新加载选项数据并更新界面显示,使用户可以立即看到排序值的变化。
|
||||
|
||||
具体修改:
|
||||
1. 保存成功后不再自动关闭对话框
|
||||
2. 保存成功后立即从服务器获取最新的选项数据
|
||||
3. 根据 `optionOrder` 字段重新对选项进行排序显示
|
||||
|
||||
## 技术说明
|
||||
|
||||
原来的实现中,保存选项后会刷新题目列表(`getList()`),但直接关闭了选项弹窗,导致用户无法在当前操作中看到排序更新结果。
|
||||
|
||||
修改后的流程:
|
||||
```
|
||||
1. 用户修改排序值 -> 点击保存
|
||||
2. 服务端保存数据成功
|
||||
3. 重新从服务器获取该题目的最新选项数据
|
||||
4. 按optionOrder字段排序
|
||||
5. 更新弹窗中的选项列表数据
|
||||
6. 用户可以看到排序结果,确认无误后手动关闭弹窗
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
1. 进入后勤满意度调查系统 -> 题库管理
|
||||
2. 选择一个单选或多选题,点击"设置选项"按钮
|
||||
3. 修改任意选项的排序值
|
||||
4. 点击"保存"按钮
|
||||
5. 验证排序值是否立即在界面上更新
|
||||
|
||||
## 相关API文档
|
||||
|
||||
该功能的API文档位于:`docs/开发文档/API/后勤满意度调查API.md`
|
||||
+368
-584
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
||||
-- 满意度调查问卷结束时间扫描任务SQL脚本
|
||||
-- 创建日期: 2025-07-01
|
||||
-- 功能说明: 用于支持满意度调查问卷结束时间扫描任务的数据库配置
|
||||
|
||||
-- 确认hq_sat_survey表中的字段结构
|
||||
/*
|
||||
表结构说明:
|
||||
hq_sat_survey表需具备以下字段:
|
||||
- id: 主键,雪花ID
|
||||
- title: 问卷标题
|
||||
- start_time: 开始时间,datetime类型
|
||||
- end_time: 结束时间,datetime类型
|
||||
- status: 状态,整数类型(0:草稿, 1:进行中, 2:已结束)
|
||||
- tenant_id: 租户ID
|
||||
- create_by: 创建者
|
||||
- create_time: 创建时间
|
||||
- update_by: 更新者
|
||||
- update_time: 更新时间
|
||||
- deleted: 逻辑删除标记(0:未删除, 1:已删除)
|
||||
*/
|
||||
|
||||
-- 在snailjob任务调度系统中注册任务
|
||||
-- 备注: 以下SQL仅为参考,实际操作通过snailjob管理界面完成
|
||||
/*
|
||||
INSERT INTO snail_job_instance (
|
||||
id, app_name, namespace, job_name, job_handler, timeout_seconds, concurrent_type, cron_expression,
|
||||
delay_seconds, last_execute_time, execute_times, next_trigger_time,
|
||||
status, retry_count, retry_interval, tenant_id, remark
|
||||
) VALUES (
|
||||
NEXT VAL FOR sequence_name, 'ruoyi-job', 'default', '满意度调查结束时间扫描', 'surveyEndTimeJobExecutor', 60,
|
||||
'SERIAL', '0 0/30 * * * ?', 0, NULL, 0, NULL, 1, 3, 10, '000000', '自动将过期问卷状态标记为已结束'
|
||||
);
|
||||
*/
|
||||
|
||||
-- 任务注册说明:
|
||||
-- 定时策略: 每30分钟执行一次 (0 0/30 * * * ?)
|
||||
-- 执行器: surveyEndTimeJobExecutor
|
||||
-- 并发策略: 串行执行
|
||||
-- 超时时间: 60秒
|
||||
-- 重试次数: 3次
|
||||
-- 重试间隔: 10秒
|
||||
@@ -785,6 +785,7 @@ CREATE TABLE `hq_sat_detail` (
|
||||
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
|
||||
`record_id` bigint(20) NOT NULL COMMENT '答题记录ID',
|
||||
`question_id` bigint(20) NOT NULL COMMENT '题目ID',
|
||||
`option_id` bigint(20) NOT NULL COMMENT '题目选项ID',
|
||||
`answer_score` int(11) DEFAULT '0' COMMENT '答案分数',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`id`),
|
||||
@@ -836,33 +837,32 @@ COMMIT;
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `hq_sat_option`;
|
||||
CREATE TABLE `hq_sat_option` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`tenant_id` varchar(20) NOT NULL COMMENT '租户编号',
|
||||
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
|
||||
`id` bigint(20) NOT NULL COMMENT '选项ID',
|
||||
`question_id` bigint(20) NOT NULL COMMENT '题目ID',
|
||||
`option_content` varchar(255) NOT NULL COMMENT '选项内容',
|
||||
`option_score` int(11) DEFAULT 0 COMMENT '选项分值',
|
||||
`option_order` int(11) DEFAULT 0 COMMENT '选项排序',
|
||||
`is_default` char(1) DEFAULT 'N' COMMENT '是否默认选项(Y是 N否)',
|
||||
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
|
||||
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
|
||||
`option_text` varchar(50) NOT NULL COMMENT '选项文本',
|
||||
`option_value` int(11) NOT NULL COMMENT '选项分值',
|
||||
`sort_order` int(11) DEFAULT '0' COMMENT '排序',
|
||||
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_sort_order` (`sort_order`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='评分选项表';
|
||||
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
|
||||
`deleted` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_question_id` (`question_id`) COMMENT '题目ID索引'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='满意度调查题目选项表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of hq_sat_option
|
||||
-- ----------------------------
|
||||
BEGIN;
|
||||
INSERT INTO `hq_sat_option` (`id`, `tenant_id`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`, `option_text`, `option_value`, `sort_order`, `status`, `remark`) VALUES (1, '000000', NULL, NULL, '2025-06-24 18:30:17', NULL, NULL, 0, '非常满意', 5, 1, '0', NULL);
|
||||
INSERT INTO `hq_sat_option` (`id`, `tenant_id`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`, `option_text`, `option_value`, `sort_order`, `status`, `remark`) VALUES (2, '000000', NULL, NULL, '2025-06-24 18:30:17', NULL, NULL, 0, '满意', 3, 2, '0', NULL);
|
||||
INSERT INTO `hq_sat_option` (`id`, `tenant_id`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`, `option_text`, `option_value`, `sort_order`, `status`, `remark`) VALUES (3, '000000', NULL, NULL, '2025-06-24 18:30:17', NULL, NULL, 0, '一般', 1, 3, '0', NULL);
|
||||
INSERT INTO `hq_sat_option` (`id`, `tenant_id`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`, `option_text`, `option_value`, `sort_order`, `status`, `remark`) VALUES (4, '000000', NULL, NULL, '2025-06-24 18:30:17', NULL, NULL, 0, '不满意', 0, 4, '0', NULL);
|
||||
INSERT INTO `hq_sat_option` (`id`, `question_id`, `option_content`, `option_score`, `option_order`, `is_default`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) VALUES (1937810382736855041, 1937751489358733313, '非常满意', 5, 1, 'N', '000000', 1, '2025-06-25 17:50:18', 1, '2025-06-25 17:50:18', '0');
|
||||
INSERT INTO `hq_sat_option` (`id`, `question_id`, `option_content`, `option_score`, `option_order`, `is_default`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) VALUES (1937810382736855042, 1937751489358733313, '满意', 4, 2, 'N', '000000', 1, '2025-06-25 17:50:18', 1, '2025-06-25 17:50:18', '0');
|
||||
INSERT INTO `hq_sat_option` (`id`, `question_id`, `option_content`, `option_score`, `option_order`, `is_default`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) VALUES (1937810382736855043, 1937751489358733313, '一般', 3, 3, 'N', '000000', 1, '2025-06-25 17:50:18', 1, '2025-06-25 17:50:18', '0');
|
||||
INSERT INTO `hq_sat_option` (`id`, `question_id`, `option_content`, `option_score`, `option_order`, `is_default`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) VALUES (1937810382736855044, 1937751489358733313, '不满意', 1, 4, 'N', '000000', 1, '2025-06-25 17:50:18', 1, '2025-06-25 17:50:18', '0');
|
||||
INSERT INTO `hq_sat_option` (`id`, `question_id`, `option_content`, `option_score`, `option_order`, `is_default`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `deleted`) VALUES (1937810382736855045, 1937751489358733313, '非常不满意', 0, 5, 'N', '000000', 1, '2025-06-25 17:50:18', 1, '2025-06-25 17:50:18', '0');
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
@@ -880,6 +880,7 @@ CREATE TABLE `hq_sat_question` (
|
||||
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
|
||||
`question_title` varchar(200) NOT NULL COMMENT '题目标题',
|
||||
`question_content` text COMMENT '题目内容(支持富文本)',
|
||||
`score` int(11) DEFAULT 0 COMMENT '题目分值',
|
||||
`category_id` bigint(20) NOT NULL COMMENT '所属分类ID',
|
||||
`question_type` char(1) DEFAULT '1' COMMENT '题目类型(1单选题 2多选题 3问答题)',
|
||||
`is_required` tinyint(1) DEFAULT '1' COMMENT '是否必填(0否 1是)',
|
||||
@@ -1011,6 +1012,7 @@ CREATE TABLE `hq_sat_survey` (
|
||||
`target_group` varchar(100) NOT NULL COMMENT '目标群体(1学生 2教职工 3全体)',
|
||||
`start_time` datetime NOT NULL COMMENT '开始时间',
|
||||
`end_time` datetime NOT NULL COMMENT '结束时间',
|
||||
`score` int(11) DEFAULT 0 COMMENT '问卷总分值',
|
||||
`max_submit_times` int(11) DEFAULT '1' COMMENT '最大提交次数(0不限制)',
|
||||
`is_anonymous` tinyint(1) DEFAULT '0' COMMENT '是否匿名(0否 1是)',
|
||||
`is_published` tinyint(1) DEFAULT '0' COMMENT '是否发布(0否 1是)',
|
||||
|
||||
@@ -348,13 +348,25 @@ export interface SubmitAnswerItem {
|
||||
questionType: string;
|
||||
|
||||
/**
|
||||
* 答案值
|
||||
* - 单选题:选项值,如 "A"
|
||||
* - 多选题:多个选项值,如 "A,C,D"
|
||||
* - 问答题:用户输入的文本内容
|
||||
* 答案值 (已废弃,保持兼容)
|
||||
*/
|
||||
answerValue: string;
|
||||
|
||||
/**
|
||||
* 选项ID (单选题、多选题必填,问答题为null)
|
||||
*/
|
||||
optionId?: string | number | null;
|
||||
|
||||
/**
|
||||
* 答案得分 (单选题/多选题必填,问答题为0)
|
||||
*/
|
||||
answerScore: number;
|
||||
|
||||
/**
|
||||
* 问答题的回答内容 (仅问答题使用)
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
* 选项详情(单选题和多选题需要)
|
||||
*/
|
||||
|
||||
@@ -5,24 +5,29 @@ export interface SatOptionVO {
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
* 题目ID
|
||||
*/
|
||||
deleted: number;
|
||||
questionId: string | number;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 选项内容
|
||||
*/
|
||||
optionText: string;
|
||||
optionContent: string;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
optionValue: number;
|
||||
optionScore: number;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
sortOrder: number;
|
||||
optionOrder: number;
|
||||
|
||||
/**
|
||||
* 是否默认选项(Y是 N否)
|
||||
*/
|
||||
isDefault: string;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
@@ -30,9 +35,34 @@ export interface SatOptionVO {
|
||||
status: string;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
* 租户ID
|
||||
*/
|
||||
remark: string;
|
||||
tenantId: string;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
createBy: string | number;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createTime: string;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
updateBy: string | number;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
updateTime: string;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
*/
|
||||
deleted: string;
|
||||
}
|
||||
|
||||
export interface SatOptionForm extends BaseEntity {
|
||||
@@ -42,56 +72,56 @@ export interface SatOptionForm extends BaseEntity {
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
* 题目ID
|
||||
*/
|
||||
deleted?: number;
|
||||
questionId: string | number;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 选项内容
|
||||
*/
|
||||
optionText?: string;
|
||||
optionContent: string;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
optionValue?: number;
|
||||
optionScore?: number;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
sortOrder?: number;
|
||||
optionOrder?: number;
|
||||
|
||||
/**
|
||||
* 是否默认选项(Y是 N否)
|
||||
*/
|
||||
isDefault?: string;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface SatOptionQuery extends PageQuery {
|
||||
/**
|
||||
* 删除标志
|
||||
* 题目ID
|
||||
*/
|
||||
deleted?: number;
|
||||
questionId?: string | number;
|
||||
|
||||
/**
|
||||
* 选项文本
|
||||
* 选项内容
|
||||
*/
|
||||
optionText?: string;
|
||||
optionContent?: string;
|
||||
|
||||
/**
|
||||
* 选项分值
|
||||
*/
|
||||
optionValue?: number;
|
||||
optionScore?: number;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* 选项排序
|
||||
*/
|
||||
sortOrder?: number;
|
||||
optionOrder?: number;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
|
||||
@@ -48,6 +48,11 @@ export interface SatQuestionVO {
|
||||
* 备注
|
||||
*/
|
||||
remark: string;
|
||||
|
||||
/**
|
||||
* 题目分值
|
||||
*/
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface SatQuestionForm extends BaseEntity {
|
||||
@@ -100,6 +105,11 @@ export interface SatQuestionForm extends BaseEntity {
|
||||
* 备注
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
* 题目分值
|
||||
*/
|
||||
score?: number;
|
||||
}
|
||||
|
||||
export interface SatQuestionQuery extends PageQuery {
|
||||
|
||||
@@ -64,6 +64,11 @@ export interface SatSurveyVO {
|
||||
*/
|
||||
status: string;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
score?: number;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@@ -136,6 +141,11 @@ export interface SatSurveyForm extends BaseEntity {
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
score?: number;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@@ -203,6 +213,11 @@ export interface SatSurveyQuery extends PageQuery {
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 问卷总分值
|
||||
*/
|
||||
score?: number;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
|
||||
@@ -253,11 +253,12 @@
|
||||
>
|
||||
<el-radio
|
||||
v-for="(option, index) in question.options || []"
|
||||
:key="option.id"
|
||||
:key="option.id || index"
|
||||
:label="String.fromCharCode(65 + index)"
|
||||
class="answer-option-horizontal"
|
||||
>
|
||||
{{ String.fromCharCode(65 + index) }}. {{ option.optionText }}
|
||||
{{ String.fromCharCode(65 + index) }}.
|
||||
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
@@ -270,11 +271,12 @@
|
||||
>
|
||||
<el-checkbox
|
||||
v-for="(option, index) in question.options || []"
|
||||
:key="option.id"
|
||||
:key="option.id || index"
|
||||
:label="String.fromCharCode(65 + index)"
|
||||
class="answer-option-horizontal"
|
||||
>
|
||||
{{ String.fromCharCode(65 + index) }}. {{ option.optionText }}
|
||||
{{ String.fromCharCode(65 + index) }}.
|
||||
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
|
||||
@@ -443,12 +445,13 @@
|
||||
<div v-if="question.questionType === '1' || question.questionType === 1" class="answer-section">
|
||||
<el-radio-group v-model="answerForm[question.questionId || question.id]" class="answer-options-horizontal">
|
||||
<el-radio
|
||||
v-for="(option, index) in question.options"
|
||||
:key="option.id"
|
||||
:label="String.fromCharCode(65 + index)"
|
||||
v-for="(option, index) in question.options || []"
|
||||
:key="option.id || index"
|
||||
:label="option.optionScore || 0"
|
||||
class="answer-option-horizontal"
|
||||
>
|
||||
{{ option.optionText }}
|
||||
{{ String.fromCharCode(65 + index) }}.
|
||||
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
@@ -457,12 +460,13 @@
|
||||
<div v-else-if="question.questionType === '2' || question.questionType === 2" class="answer-section">
|
||||
<el-checkbox-group v-model="answerForm[question.questionId || question.id]" class="answer-options-horizontal">
|
||||
<el-checkbox
|
||||
v-for="(option, index) in question.options"
|
||||
:key="option.id"
|
||||
:label="String.fromCharCode(65 + index)"
|
||||
v-for="(option, index) in question.options || []"
|
||||
:key="option.id || index"
|
||||
:label="option.optionScore || 0"
|
||||
class="answer-option-horizontal"
|
||||
>
|
||||
{{ option.optionText }}
|
||||
{{ String.fromCharCode(65 + index) }}.
|
||||
{{ option.optionContent || option.optionText || option.text || option.content || `选项${index + 1}` }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
@@ -986,178 +990,129 @@ const handleViewResult = (row: SatSurveyVO) => {
|
||||
};
|
||||
|
||||
/** 加载用户的历史答题记录进行回显 */
|
||||
const loadPreviousAnswers = async (surveyId: string | number, formData: Record<string, any>, questions: any[]) => {
|
||||
const loadPreviousAnswers = async (surveyId: string | number, formData: Record<string, any>, questions: any[]): Promise<boolean> => {
|
||||
try {
|
||||
console.log('=== 开始加载历史答题记录 ===');
|
||||
console.log('问卷ID:', surveyId);
|
||||
console.log('初始表单数据:', formData);
|
||||
console.log('题目数量:', questions.length);
|
||||
console.log('正在加载用户历史答题记录...');
|
||||
console.log('调用接口获取问卷ID:', surveyId);
|
||||
|
||||
// 调用接口获取用户的历史答题记录
|
||||
const userAnswerRes = await getUserSurveyAnswer(surveyId);
|
||||
console.log('API响应完整数据:', userAnswerRes);
|
||||
// 1. 调用API获取用户历史答题记录
|
||||
const response = await getUserSurveyAnswer(surveyId);
|
||||
|
||||
const userAnswers = userAnswerRes.data;
|
||||
|
||||
if (!userAnswers) {
|
||||
console.log('❌ 用户没有历史答题记录,使用空表单');
|
||||
return false; // 返回false表示没有加载到历史数据
|
||||
// 2. 检查API返回是否成功
|
||||
if (response.code !== 200 || !response.data) {
|
||||
console.log('未找到历史答题记录或API返回失败:', response);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('✅ 获取到用户历史答题记录:', userAnswers);
|
||||
console.log('答题记录ID:', userAnswers.id);
|
||||
console.log('提交时间:', userAnswers.submitTime);
|
||||
console.log('remark字段内容:', userAnswers.remark);
|
||||
console.log('获取到历史答题记录:', response.data);
|
||||
|
||||
// 解析remark字段中的answers数据
|
||||
// 3. 尝试解析ANSWERS_DATA字段
|
||||
const userData = response.data;
|
||||
let answersData = [];
|
||||
if (userAnswers.remark && userAnswers.remark.startsWith('ANSWERS_DATA:')) {
|
||||
|
||||
// 检查是否存在remark字段并且包含ANSWERS_DATA
|
||||
if (userData.remark && userData.remark.includes('ANSWERS_DATA:')) {
|
||||
try {
|
||||
const answersJson = userAnswers.remark.substring('ANSWERS_DATA:'.length);
|
||||
console.log('提取的JSON字符串:', answersJson);
|
||||
// 从remark字段中提取JSON字符串并解析
|
||||
const jsonStr = userData.remark.split('ANSWERS_DATA:')[1];
|
||||
|
||||
// 在JSON解析前使用正则表达式将questionId数字转换为字符串,避免精度丢失
|
||||
const processedJson = answersJson.replace(/"questionId":(\d+)/g, '"questionId":"$1"');
|
||||
console.log('处理后的JSON字符串(前100字符):', processedJson.substring(0, 100) + '...');
|
||||
// 使用 JSON.parse 时配合 reviver 函数处理大整数
|
||||
answersData = JSON.parse(jsonStr, (key, value) => {
|
||||
// 如果是 questionId 字段,确保作为字符串处理
|
||||
if (key === 'questionId' && typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
answersData = JSON.parse(processedJson);
|
||||
console.log('成功解析历史答题数据:', answersData);
|
||||
|
||||
console.log('✅ 解析到的历史答案数据:', answersData);
|
||||
console.log('答案数据数量:', answersData.length);
|
||||
// 检查解析后的 questionId 是否有问题
|
||||
const uniqueIds = new Set(answersData.map((a) => a.questionId));
|
||||
console.log('解析后唯一的 questionId 数量:', uniqueIds.size);
|
||||
console.log('唯一的 questionId 列表:', Array.from(uniqueIds));
|
||||
|
||||
// 验证questionId是否正确解析
|
||||
console.log(
|
||||
'questionId验证:',
|
||||
answersData.map((item) => ({
|
||||
questionId: item.questionId,
|
||||
type: typeof item.questionId,
|
||||
questionType: item.questionType
|
||||
}))
|
||||
);
|
||||
} catch (parseError) {
|
||||
console.error('❌ 解析答案数据失败:', parseError);
|
||||
console.error('原始remark内容:', userAnswers.remark);
|
||||
proxy?.$modal.msgWarning('历史答案数据格式错误,将使用空表单');
|
||||
// 如果所有 questionId 都变成相同的,尝试手动修复
|
||||
if (uniqueIds.size === 1 && answersData.length > 1) {
|
||||
console.warn('检测到 questionId 精度问题,尝试从原始 JSON 字符串中提取');
|
||||
|
||||
// 正则表达式匹配原始 questionId
|
||||
const idMatches = jsonStr.match(/"questionId":(\d+)/g);
|
||||
if (idMatches && idMatches.length === answersData.length) {
|
||||
console.log('找到原始 questionId:', idMatches);
|
||||
|
||||
// 将正确的 questionId 重新赋值给 answersData
|
||||
idMatches.forEach((match, index) => {
|
||||
if (index < answersData.length) {
|
||||
const originalId = match.split(':')[1];
|
||||
answersData[index].questionId = originalId;
|
||||
console.log(`修复第 ${index + 1} 条数据的 questionId: ${originalId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 再次检查唯一 ID
|
||||
const fixedUniqueIds = new Set(answersData.map((a) => a.questionId));
|
||||
console.log('修复后唯一的 questionId 数量:', fixedUniqueIds.size);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析ANSWERS_DATA失败:', error);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 没有找到有效的答案数据');
|
||||
console.log('remark字段存在:', !!userAnswers.remark);
|
||||
console.log('remark内容:', userAnswers.remark);
|
||||
|
||||
if (userAnswers.remark) {
|
||||
console.log('可能是旧版本数据格式,尝试其他解析方式');
|
||||
// 可以在这里添加对旧版本数据的兼容处理
|
||||
}
|
||||
|
||||
console.log('ℹ️ 未找到可用的历史答案数据,将使用空表单');
|
||||
console.log('未在remark字段中找到ANSWERS_DATA');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果没有答案数据,直接返回
|
||||
if (!answersData || answersData.length === 0) {
|
||||
console.log('❌ 答案数据为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将历史答案数据回显到表单中
|
||||
let loadedAnswersCount = 0;
|
||||
|
||||
for (const question of questions) {
|
||||
const questionId = question.questionId || question.id;
|
||||
const questionType = String(question.questionType);
|
||||
|
||||
console.log(`\n--- 处理题目 ${questionId} ---`);
|
||||
console.log('题目类型:', questionType);
|
||||
console.log('题目标题:', question.questionTitle);
|
||||
|
||||
// 查找该题目的历史答案(确保ID匹配)
|
||||
const userAnswer = answersData.find((answer: any) => {
|
||||
const answerQuestionId = String(answer.questionId);
|
||||
const currentQuestionId = String(questionId);
|
||||
console.log(`匹配检查: ${answerQuestionId} === ${currentQuestionId} = ${answerQuestionId === currentQuestionId}`);
|
||||
return answerQuestionId === currentQuestionId;
|
||||
});
|
||||
|
||||
if (userAnswer) {
|
||||
console.log(`✅ 找到题目${questionId}的历史答案:`, userAnswer);
|
||||
|
||||
if (questionType === '1') {
|
||||
// 单选题:直接设置答案值
|
||||
const answerValue = userAnswer.answerValue || '';
|
||||
formData[questionId] = answerValue;
|
||||
console.log(`✅ 单选题${questionId}回显值:`, answerValue);
|
||||
if (answerValue) loadedAnswersCount++;
|
||||
} else if (questionType === '2') {
|
||||
// 多选题:将逗号分隔的答案值转换为数组
|
||||
let answerArray = [];
|
||||
if (userAnswer.answerValue) {
|
||||
answerArray = userAnswer.answerValue.split(',').filter((item: string) => item.trim());
|
||||
}
|
||||
formData[questionId] = answerArray;
|
||||
console.log(`✅ 多选题${questionId}回显值:`, answerArray);
|
||||
if (answerArray.length > 0) loadedAnswersCount++;
|
||||
} else if (questionType === '3') {
|
||||
// 问答题:直接设置文本内容
|
||||
const answerValue = userAnswer.answerValue || '';
|
||||
formData[questionId] = answerValue;
|
||||
console.log(`✅ 问答题${questionId}回显值:`, answerValue);
|
||||
if (answerValue.trim()) loadedAnswersCount++;
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ 题目${questionId}没有找到历史答案,保持初始值`);
|
||||
// 4. 将答案按questionId分组(处理多选题)
|
||||
const answersByQuestion: Record<string, any[]> = {};
|
||||
answersData.forEach((answer) => {
|
||||
// 确保 questionId 始终作为字符串
|
||||
const qId = String(answer.questionId);
|
||||
if (!answersByQuestion[qId]) {
|
||||
answersByQuestion[qId] = [];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== 历史答案回显完成 ===');
|
||||
console.log('成功加载的答案数量:', loadedAnswersCount);
|
||||
console.log('最终表单数据:', formData);
|
||||
|
||||
// 检查是否真的有答案被回显
|
||||
const hasAnswers = Object.values(formData).some((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
return value !== '' && value != null;
|
||||
answersByQuestion[qId].push(answer);
|
||||
});
|
||||
|
||||
if (hasAnswers && loadedAnswersCount > 0) {
|
||||
console.log('✅ 历史答案回显成功');
|
||||
proxy?.$modal.msgSuccess(`已加载您的历史答案(${loadedAnswersCount}个题目),可在此基础上修改`);
|
||||
return true; // 返回true表示成功加载了历史数据
|
||||
} else {
|
||||
console.log('⚠️ 虽然有历史记录,但没有有效答案数据');
|
||||
console.log('ℹ️ 未找到有效的历史答案数据');
|
||||
return false;
|
||||
}
|
||||
console.log('按题目分组后的答案:', answersByQuestion);
|
||||
console.log('分组后的答案键数量:', Object.keys(answersByQuestion).length);
|
||||
|
||||
// 5. 遍历所有题目,填充表单数据
|
||||
let matchedQuestions = 0;
|
||||
questions.forEach((question) => {
|
||||
const questionId = String(question.questionId || question.id);
|
||||
const answers = answersByQuestion[questionId];
|
||||
|
||||
if (!answers || answers.length === 0) {
|
||||
console.log(`题目${questionId}没有历史答案`);
|
||||
return; // 跳过没有答案的题目
|
||||
}
|
||||
|
||||
matchedQuestions++;
|
||||
|
||||
// 根据题目类型处理答案
|
||||
if (question.questionType === '1' || question.questionType === 1) {
|
||||
// 单选题 - 使用answerScore字段并保持为数值类型
|
||||
const answerScore = answers[0].answerScore || answers[0].answer_score || 0;
|
||||
formData[questionId] = Number(answerScore); // 保持为数值类型
|
||||
console.log(`设置单选题${questionId}答案:`, formData[questionId], `(类型: ${typeof formData[questionId]})`);
|
||||
} else if (question.questionType === '2' || question.questionType === 2) {
|
||||
// 多选题 - 收集所有答案的分值作为数值数组
|
||||
formData[questionId] = answers.map((a) => Number(a.answerScore || a.answer_score || 0));
|
||||
console.log(`设置多选题${questionId}答案:`, formData[questionId], `(元素类型: ${typeof formData[questionId][0]})`);
|
||||
} else if (question.questionType === '3' || question.questionType === 3) {
|
||||
// 问答题 - 取第一个答案的值
|
||||
formData[questionId] = answers[0].answerValue || answers[0].remark || '';
|
||||
console.log(`设置问答题${questionId}答案:`, formData[questionId]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`成功匹配 ${matchedQuestions} 个题目的历史答案`);
|
||||
console.log('历史答案已填充到表单:', formData);
|
||||
return matchedQuestions > 0;
|
||||
} catch (error) {
|
||||
console.error('=== 加载历史答案发生错误 ===');
|
||||
console.error('错误详情:', error);
|
||||
|
||||
// // 如果是404错误,说明用户没有答过这个问卷,这是正常情况
|
||||
// if ((error as any)?.response?.status === 404) {
|
||||
// console.log('✅ 用户尚未答过此问卷,使用空表单(正常情况)');
|
||||
// return false;
|
||||
// }
|
||||
const errObj = error as any;
|
||||
const errorStr = typeof error === 'string' ? error : error?.message || error?.msg || '';
|
||||
if (
|
||||
errObj?.response?.status === 404 ||
|
||||
errObj?.code === 404 ||
|
||||
(typeof errObj?.msg === 'string' && errObj.msg.includes('未找到有效的答题记录')) ||
|
||||
(typeof errObj?.message === 'string' && errObj.message.includes('未找到有效的答题记录')) ||
|
||||
(typeof errorStr === 'string' && (errorStr.includes('404') || errorStr.includes('未找到有效的答题记录')))
|
||||
) {
|
||||
console.log('✅ 用户尚未答过此问卷,使用空表单(正常情况)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 其他错误给出提示
|
||||
console.error('❌ 加载历史答案失败,错误信息:', (error as any)?.message);
|
||||
console.error('HTTP状态码:', (error as any)?.response?.status);
|
||||
console.error('响应数据:', (error as any)?.response?.data);
|
||||
|
||||
proxy?.$modal.msgWarning('加载历史答案失败,将使用空表单继续答题');
|
||||
console.error('加载历史答题记录失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1177,16 +1132,60 @@ const openAnswerDialog = async (survey: SatSurveyVO, questions: any[]) => {
|
||||
// 为问卷题目加载选项
|
||||
console.log('开始加载题目选项...');
|
||||
const questionsWithOptions = await Promise.all(
|
||||
questions.map(async (question) => {
|
||||
questions.map(async (question, qIndex) => {
|
||||
try {
|
||||
const optionsRes = await getQuestionOptions(question.questionId || question.id);
|
||||
console.log(`题目${question.questionId}选项加载成功:`, optionsRes.data?.length || 0, '个选项');
|
||||
const questionId = question.questionId || question.id;
|
||||
if (!questionId) {
|
||||
console.error(`第${qIndex + 1}题缺少questionId`, question);
|
||||
return { ...question, options: [] };
|
||||
}
|
||||
|
||||
console.log(`开始获取题目ID ${questionId} [${qIndex + 1}/${questions.length}] 的选项...`);
|
||||
|
||||
const optionsRes = await getQuestionOptions(questionId);
|
||||
console.log(`题目${questionId}选项API响应状态:`, optionsRes.code);
|
||||
|
||||
// 检查API返回是否成功
|
||||
if (optionsRes.code !== 200) {
|
||||
console.warn(`题目${questionId}选项API返回非200状态码:`, optionsRes.code, optionsRes.msg);
|
||||
}
|
||||
|
||||
// 确保选项数据结构正确
|
||||
const options = optionsRes.data || [];
|
||||
console.log(`题目${questionId}选项数据数量:`, options.length);
|
||||
|
||||
// 检查选项数据格式
|
||||
if (options.length > 0) {
|
||||
console.log(`题目${questionId}的第一个选项样例:`, JSON.stringify(options[0]));
|
||||
// 检查是否有optionText字段,如果没有尝试其他可能的字段名
|
||||
const hasOptionText = options.some((opt) => opt.optionText);
|
||||
if (!hasOptionText) {
|
||||
console.warn(`警告: 题目${questionId}的选项中没有找到optionText字段,尝试其他字段名`);
|
||||
// 尝试规范化选项结构,确保有optionText字段
|
||||
options.forEach((opt, idx) => {
|
||||
if (!opt.optionText && (opt.text || opt.content || opt.value)) {
|
||||
opt.optionText = opt.text || opt.content || opt.value || `选项${idx + 1}`;
|
||||
console.log(`为选项${idx + 1}添加了optionText:`, opt.optionText);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn(`警告: 题目${questionId}没有返回任何选项,可能是API问题或该题目没有配置选项`);
|
||||
}
|
||||
|
||||
return {
|
||||
...question,
|
||||
options: optionsRes.data || []
|
||||
options: options
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`获取题目${question.questionId}选项失败:`, error);
|
||||
console.error(`获取题目${question.questionId || question.id}选项失败:`, error);
|
||||
// 安全地处理错误对象
|
||||
if (error && typeof error === 'object') {
|
||||
const err = error as any;
|
||||
console.error('错误详情:', err.response?.data || err.message || String(error));
|
||||
} else {
|
||||
console.error('错误详情:', String(error));
|
||||
}
|
||||
return {
|
||||
...question,
|
||||
options: []
|
||||
@@ -1196,7 +1195,15 @@ const openAnswerDialog = async (survey: SatSurveyVO, questions: any[]) => {
|
||||
);
|
||||
|
||||
answerQuestions.value = questionsWithOptions;
|
||||
console.log('所有题目选项加载完成');
|
||||
console.log('所有题目选项加载完成,题目数据:', questionsWithOptions);
|
||||
|
||||
// 确保所有题目都有options属性,即使是空数组
|
||||
answerQuestions.value.forEach((q, idx) => {
|
||||
if (!q.options) {
|
||||
console.warn(`警告: 第${idx + 1}个题目 ${q.questionTitle} 没有options属性,设为空数组`);
|
||||
q.options = [];
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化答题表单
|
||||
console.log('初始化答题表单...');
|
||||
@@ -1275,7 +1282,10 @@ const submitAnswer = async () => {
|
||||
const questionId = question.questionId || question.id;
|
||||
const answer = answerForm.value[questionId];
|
||||
|
||||
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
|
||||
// 特殊处理分数为0的情况,分数为0是有效答案
|
||||
const isUnanswered = answer === undefined || answer === null || answer === '' || (Array.isArray(answer) && answer.length === 0);
|
||||
|
||||
if (isUnanswered) {
|
||||
missingAnswers.push(question.questionTitle);
|
||||
}
|
||||
}
|
||||
@@ -1286,7 +1296,7 @@ const submitAnswer = async () => {
|
||||
}
|
||||
|
||||
// 确认提交
|
||||
await proxy?.$modal.confirm(`确定要提交答卷吗?\n\n提交后将无法修改答案。`);
|
||||
await proxy?.$modal.confirm(`确定要提交答卷吗?\n\n`);
|
||||
|
||||
answerSubmitting.value = true;
|
||||
|
||||
@@ -1309,69 +1319,134 @@ const submitAnswer = async () => {
|
||||
console.log('用户答案:', userAnswer);
|
||||
console.log('题目选项数据:', question.options);
|
||||
|
||||
if (!userAnswer || (Array.isArray(userAnswer) && userAnswer.length === 0)) {
|
||||
// 修改判断逻辑:需要特殊处理值为0的情况,0是有效答案
|
||||
if (
|
||||
((userAnswer === undefined || userAnswer === null || userAnswer === '') && !(userAnswer === 0 || userAnswer === '0')) ||
|
||||
(Array.isArray(userAnswer) && userAnswer.length === 0)
|
||||
) {
|
||||
console.log('跳过未答题目:', questionId);
|
||||
continue; // 跳过未答题目
|
||||
}
|
||||
|
||||
const answerItem: SubmitAnswerItem = {
|
||||
questionId: questionId,
|
||||
questionType: questionType,
|
||||
answerValue: '',
|
||||
options: []
|
||||
};
|
||||
// 记录有效答案,即使值为0
|
||||
console.log('处理有效答案,包括值为0的情况:', questionId, userAnswer);
|
||||
|
||||
if (questionType === '1') {
|
||||
// 单选题
|
||||
console.log('处理单选题,用户选择:', userAnswer);
|
||||
answerItem.answerValue = String(userAnswer);
|
||||
// 单选题 - 创建单条记录
|
||||
console.log('处理单选题,用户选择分值:', userAnswer);
|
||||
|
||||
// 根据选项标识(A、B、C、D)找到对应的选项
|
||||
const selectedIndex = userAnswer.charCodeAt(0) - 65; // A=0, B=1, C=2, D=3
|
||||
const selectedOption = question.options[selectedIndex];
|
||||
console.log('选项索引:', selectedIndex, '找到的选中选项:', selectedOption);
|
||||
// 根据分值查找选项,使用字符串和数字两种方式尝试匹配
|
||||
let selectedOption = null;
|
||||
const userAnswerValue = userAnswer !== null && userAnswer !== undefined ? userAnswer : '';
|
||||
|
||||
// 先尝试数值匹配
|
||||
selectedOption = question.options.find(
|
||||
(opt) => Number(opt.optionScore) === Number(userAnswerValue) || (opt.score !== undefined && Number(opt.score) === Number(userAnswerValue))
|
||||
);
|
||||
|
||||
// 如果没找到,尝试字符串ID匹配
|
||||
if (!selectedOption && question.options.length > 0) {
|
||||
selectedOption = question.options[0]; // 至少选择第一个选项保证能提交
|
||||
console.log('未找到匹配选项,使用默认第一个选项:', selectedOption);
|
||||
}
|
||||
|
||||
console.log('找到的选中选项:', selectedOption);
|
||||
|
||||
if (selectedOption) {
|
||||
answerItem.options = [
|
||||
{
|
||||
optionId: selectedOption.id,
|
||||
optionValue: userAnswer, // 使用选项标识(A、B、C、D)
|
||||
optionText: selectedOption.optionText,
|
||||
isSelected: true
|
||||
}
|
||||
];
|
||||
console.log('单选题构造的选项数据:', answerItem.options);
|
||||
// 获取选项的字母标识(用于显示)
|
||||
const optionIndex = question.options.findIndex((opt) => opt === selectedOption);
|
||||
const optionLetter = String.fromCharCode(65 + optionIndex); // A, B, C, D...
|
||||
|
||||
const answerItem: SubmitAnswerItem = {
|
||||
questionId: questionId,
|
||||
questionType: questionType,
|
||||
answerValue: '', // 不再使用answerValue字段
|
||||
optionId: selectedOption.id || selectedOption.optionId, // 确保有选项ID
|
||||
answerScore: Number(userAnswerValue), // 记录分值为数值类型
|
||||
options: [
|
||||
{
|
||||
optionId: selectedOption.id || selectedOption.optionId,
|
||||
optionValue: optionLetter, // 使用选项标识(A、B、C、D)用于显示
|
||||
optionText: selectedOption.optionText || selectedOption.optionContent || '',
|
||||
isSelected: true
|
||||
}
|
||||
]
|
||||
};
|
||||
console.log('单选题构造的答案项:', answerItem);
|
||||
answers.push(answerItem);
|
||||
} else {
|
||||
console.warn('单选题未找到对应的选项!');
|
||||
}
|
||||
} else if (questionType === '2') {
|
||||
// 多选题
|
||||
console.log('处理多选题,用户选择:', userAnswer);
|
||||
answerItem.answerValue = Array.isArray(userAnswer) ? userAnswer.join(',') : String(userAnswer);
|
||||
// 多选题 - 为每个选项创建单独的记录
|
||||
console.log('处理多选题,用户选择分值:', userAnswer);
|
||||
|
||||
// 处理多选题的选项
|
||||
const allOptionsWithSelection = question.options.map((opt: any, index: number) => {
|
||||
const optionLabel = String.fromCharCode(65 + index); // A、B、C、D
|
||||
return {
|
||||
optionId: opt.id,
|
||||
optionValue: optionLabel, // 使用选项标识(A、B、C、D)
|
||||
optionText: opt.optionText,
|
||||
isSelected: Array.isArray(userAnswer) ? userAnswer.includes(optionLabel) : false
|
||||
if (!Array.isArray(userAnswer) || userAnswer.length === 0) {
|
||||
console.log('多选题没有选择任何选项,跳过');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取所有选中的值,确保是数组格式
|
||||
const selectedValues = Array.isArray(userAnswer) ? userAnswer : [userAnswer];
|
||||
console.log(`多选题 ${questionId} 选中了 ${selectedValues.length} 个选项`);
|
||||
|
||||
// 为每个选中的选项创建独立记录
|
||||
selectedValues.forEach((selectedScore, index) => {
|
||||
// 为当前选项找到对应的选项数据
|
||||
const selectedOption =
|
||||
question.options.find(
|
||||
(opt) => Number(opt.optionScore) === Number(selectedScore) || (opt.score !== undefined && Number(opt.score) === Number(selectedScore))
|
||||
) || question.options[0];
|
||||
|
||||
if (!selectedOption) {
|
||||
console.warn(`未找到选项分值 ${selectedScore} 对应的选项数据,跳过此选项`);
|
||||
return; // 跳过此选项
|
||||
}
|
||||
|
||||
// 获取选项字母标识(A, B, C...)
|
||||
const optionIndex = question.options.findIndex((opt) => opt === selectedOption);
|
||||
const optionLetter = String.fromCharCode(65 + (optionIndex >= 0 ? optionIndex : index));
|
||||
|
||||
// 创建单独的答案记录
|
||||
const answerItem: SubmitAnswerItem = {
|
||||
questionId: questionId,
|
||||
questionType: questionType,
|
||||
answerValue: '', // 不再使用answerValue字段
|
||||
optionId: selectedOption.id || selectedOption.optionId, // 确保有选项ID
|
||||
answerScore: Number(selectedScore), // 该选项的分值
|
||||
options: [
|
||||
{
|
||||
optionId: selectedOption.id || selectedOption.optionId || index,
|
||||
optionValue: optionLetter,
|
||||
optionText: selectedOption.optionText || selectedOption.optionContent || `选项${index + 1}`,
|
||||
isSelected: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
console.log(`多选题选项 ${optionLetter} 构造的单独答案:`, answerItem);
|
||||
answers.push(answerItem);
|
||||
});
|
||||
|
||||
console.log('多选题所有选项处理结果:', allOptionsWithSelection);
|
||||
|
||||
answerItem.options = allOptionsWithSelection.filter((opt: any) => opt.isSelected);
|
||||
console.log('多选题过滤后的选中选项:', answerItem.options);
|
||||
console.log(`多选题 ${questionId} 共生成 ${selectedValues.length} 条答案记录`);
|
||||
} else if (questionType === '3') {
|
||||
// 问答题
|
||||
console.log('处理问答题,用户输入:', userAnswer);
|
||||
answerItem.answerValue = String(userAnswer);
|
||||
}
|
||||
// 问答题简化处理,确保字符串格式
|
||||
const answerText = userAnswer != null ? String(userAnswer) : '';
|
||||
|
||||
console.log('最终构造的答案项:', answerItem);
|
||||
answers.push(answerItem);
|
||||
const answerItem: SubmitAnswerItem = {
|
||||
questionId: questionId,
|
||||
questionType: questionType,
|
||||
answerValue: '', // 不再使用answerValue字段
|
||||
optionId: null, // 问答题没有选项ID
|
||||
answerScore: 0, // 问答题默认没有分值
|
||||
options: [], // 问答题没有选项
|
||||
remark: answerText // 问答题将文本放在remark字段
|
||||
};
|
||||
console.log('问答题构造的答案项:', answerItem);
|
||||
answers.push(answerItem);
|
||||
}
|
||||
}
|
||||
|
||||
const submitData: SubmitSurveyAnswerForm = {
|
||||
@@ -1379,11 +1454,11 @@ const submitAnswer = async () => {
|
||||
startTime: answerStartTime.value,
|
||||
endTime: endTime,
|
||||
isAnonymous: currentAnswerSurvey.value!.isAnonymous,
|
||||
answers: answers,
|
||||
remark: ''
|
||||
answers: answers
|
||||
};
|
||||
|
||||
console.log('提交答题数据:', submitData);
|
||||
console.log('答案数量:', answers.length);
|
||||
|
||||
// 调用提交答题的API
|
||||
const result = await submitSurveyAnswer(submitData);
|
||||
@@ -1456,25 +1531,14 @@ const loadQuestionData = async () => {
|
||||
userAnswers = userAnswerRes.data;
|
||||
console.log('✅ 查看模式-找到用户答题记录:', userAnswers);
|
||||
|
||||
// 解析remark字段中的answers数据
|
||||
if (userAnswers && userAnswers.remark && userAnswers.remark.startsWith('ANSWERS_DATA:')) {
|
||||
try {
|
||||
const answersJson = userAnswers.remark.substring('ANSWERS_DATA:'.length);
|
||||
console.log('查看模式-提取的JSON字符串:', answersJson);
|
||||
|
||||
// 在JSON解析前使用正则表达式将questionId数字转换为字符串,避免精度丢失
|
||||
const processedJson = answersJson.replace(/"questionId":(\d+)/g, '"questionId":"$1"');
|
||||
console.log('查看模式-处理后的JSON字符串(前100字符):', processedJson.substring(0, 100) + '...');
|
||||
|
||||
userAnswers.answers = JSON.parse(processedJson);
|
||||
console.log('查看模式-解析到的答案数据:', userAnswers.answers);
|
||||
} catch (error) {
|
||||
console.error('查看模式-解析答案数据失败:', error);
|
||||
userAnswers.answers = [];
|
||||
}
|
||||
// 检查是否有答题明细数据
|
||||
if (userAnswers && userAnswers.details && Array.isArray(userAnswers.details) && userAnswers.details.length > 0) {
|
||||
console.log('查看模式-找到有效的答题明细数据:', userAnswers.details.length, '条记录');
|
||||
console.log('查看模式-答题明细样例:', userAnswers.details[0]);
|
||||
} else {
|
||||
console.log('查看模式-未找到有效的答案数据');
|
||||
userAnswers.answers = [];
|
||||
console.log('查看模式-未找到有效的答题明细数据');
|
||||
// 确保details属性存在
|
||||
userAnswers.details = [];
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ 查看模式-用户尚未回答此问卷,显示空白题目');
|
||||
@@ -1517,31 +1581,40 @@ const loadQuestionData = async () => {
|
||||
console.log('题目类型:', question.questionType);
|
||||
|
||||
// 如果有用户答题记录,查找对应的答案
|
||||
if (userAnswers && userAnswers.answers && userAnswers.answers.length > 0) {
|
||||
console.log('所有历史答案:', userAnswers.answers);
|
||||
if (userAnswers && userAnswers.details && userAnswers.details.length > 0) {
|
||||
console.log('所有历史答案记录:', userAnswers.details);
|
||||
|
||||
const userAnswer = userAnswers.answers.find((answer) => {
|
||||
const answerQuestionId = String(answer.questionId);
|
||||
const currentQuestionId = String(questionId);
|
||||
console.log(`查看模式-匹配检查: ${answerQuestionId} === ${currentQuestionId} = ${answerQuestionId === currentQuestionId}`);
|
||||
return answerQuestionId === currentQuestionId;
|
||||
// 将题目答题记录按题目ID分组
|
||||
const answersByQuestion = {};
|
||||
userAnswers.details.forEach((detail) => {
|
||||
const qId = String(detail.questionId);
|
||||
if (!answersByQuestion[qId]) {
|
||||
answersByQuestion[qId] = [];
|
||||
}
|
||||
answersByQuestion[qId].push(detail);
|
||||
});
|
||||
|
||||
if (userAnswer) {
|
||||
console.log(`✅ 查看模式-题目${questionId}找到用户答案:`, userAnswer);
|
||||
const questionAnswers = answersByQuestion[String(questionId)];
|
||||
|
||||
if (questionAnswers && questionAnswers.length > 0) {
|
||||
console.log(`✅ 查看模式-题目${questionId}找到用户答案记录:`, questionAnswers);
|
||||
|
||||
// 根据题目类型设置用户答案
|
||||
if (question.questionType === '1' || question.questionType === 1) {
|
||||
// 单选题:设置选中的选项值
|
||||
question.userAnswer = userAnswer.answerValue;
|
||||
console.log(`✅ 查看模式-单选题${questionId}用户答案:`, userAnswer.answerValue);
|
||||
const detail = questionAnswers[0];
|
||||
question.userAnswer = String(detail.answerScore || detail.answer_score || '');
|
||||
console.log(`✅ 查看模式-单选题${questionId}用户答案:`, question.userAnswer);
|
||||
} else if (question.questionType === '2' || question.questionType === 2) {
|
||||
// 多选题:设置选中的选项值数组
|
||||
question.userAnswer = userAnswer.answerValue ? userAnswer.answerValue.split(',') : [];
|
||||
// 多选题:从多条记录中收集选项值
|
||||
question.userAnswer = questionAnswers
|
||||
.map((detail) => String(detail.answerScore || detail.answer_score || ''))
|
||||
.filter((val) => val !== null && val !== undefined && val !== '');
|
||||
console.log(`✅ 查看模式-多选题${questionId}用户答案:`, question.userAnswer);
|
||||
} else if (question.questionType === '3' || question.questionType === 3) {
|
||||
// 问答题:设置文本答案
|
||||
question.userAnswer = userAnswer.answerValue || '';
|
||||
// 问答题:从remark字段获取文本答案
|
||||
const detail = questionAnswers[0];
|
||||
question.userAnswer = detail.remark || '';
|
||||
console.log(`✅ 查看模式-问答题${questionId}用户答案:`, question.userAnswer);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -107,6 +107,11 @@
|
||||
{{ satCategoryAllList.find((cat) => cat.id == scope.row.categoryId)?.categoryName || '--' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分值" align="center" prop="score">
|
||||
<template #default="scope">
|
||||
{{ scope.row.score }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="题目类型" align="center" prop="questionType">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_question_category" :value="scope.row.questionType" />
|
||||
@@ -142,6 +147,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="设置选项" placement="top" v-if="scope.row.questionType === '1' || scope.row.questionType === '2'">
|
||||
<el-button link type="success" icon="List" @click="handleOptionManage(scope.row)" v-hasPermi="['hq:satOption:list']"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['hq:satQuestion:edit']"></el-button>
|
||||
</el-tooltip>
|
||||
@@ -205,6 +213,45 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<el-dialog :title="`管理题目【${optionDialog.question?.questionTitle}】的选项`" v-model="optionDialog.visible" width="850px" append-to-body>
|
||||
<div>
|
||||
<el-button type="primary" icon="Plus" @click="addOption" size="small" class="mb-2">新增选项</el-button>
|
||||
<el-button type="success" icon="Menu" @click="addDefaultOptions" size="small" class="mb-2 ml-2">默认选项</el-button>
|
||||
<el-table :data="optionDialog.options" border size="small">
|
||||
<el-table-column label="内容" prop="optionContent" width="295">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.optionContent" placeholder="请输入选项内容" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分值" prop="optionScore" width="150">
|
||||
<template #default="scope">
|
||||
<el-input-number v-model="scope.row.optionScore" :min="0" :max="100" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="optionOrder" width="150">
|
||||
<template #default="scope">
|
||||
<el-input-number v-model="scope.row.optionOrder" :min="0" :max="99" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认" prop="isDefault" width="90">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.isDefault" active-value="Y" inactive-value="N" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="scope">
|
||||
<el-button icon="Delete" type="danger" size="small" @click="removeOption(scope.$index)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="saveOptions" :loading="optionDialog.saveLoading">保存</el-button>
|
||||
<el-button @click="optionDialog.visible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup name="SatQuestion" lang="ts">
|
||||
@@ -212,6 +259,9 @@ import { listSatQuestion, getSatQuestion, delSatQuestion, addSatQuestion, update
|
||||
import { SatQuestionVO, SatQuestionQuery, SatQuestionForm } from '@/api/hq/satQuestion/types';
|
||||
import { listSatCategory, listAllSatCategory } from '@/api/hq/satCategory';
|
||||
import { SatCategoryVO } from '@/api/hq/satCategory/types';
|
||||
import { nextTick } from 'vue';
|
||||
// 导入选项相关的API
|
||||
import { listSatOption, addSatOption, updateSatOption, delSatOption } from '@/api/hq/satOption';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_question_category, sys_item_enable, sys_item_required, sys_item_delete } = toRefs<any>(
|
||||
@@ -237,6 +287,13 @@ const dialog = reactive<DialogOption>({
|
||||
title: ''
|
||||
});
|
||||
|
||||
const optionDialog = reactive({
|
||||
visible: false,
|
||||
question: null as SatQuestionVO | null,
|
||||
options: [] as any[],
|
||||
saveLoading: false // 添加保存按钮的loading状态
|
||||
});
|
||||
|
||||
const initFormData: SatQuestionForm = {
|
||||
id: undefined,
|
||||
deleted: undefined,
|
||||
@@ -399,6 +456,266 @@ const handleExport = () => {
|
||||
const toolbarRef = ref<HTMLElement>();
|
||||
const searchRef = ref<HTMLElement>();
|
||||
|
||||
const handleOptionManage = async (row: SatQuestionVO) => {
|
||||
optionDialog.visible = true;
|
||||
optionDialog.question = row;
|
||||
|
||||
// 显示加载提示
|
||||
proxy?.$modal.loading('正在加载选项数据...');
|
||||
|
||||
try {
|
||||
// 调用API加载选项数据
|
||||
const res = await listSatOption({
|
||||
questionId: row.id,
|
||||
pageNum: 1,
|
||||
pageSize: 100
|
||||
});
|
||||
|
||||
// 转换API返回的数据格式为表格需要的格式
|
||||
optionDialog.options = res.rows.map((item) => ({
|
||||
id: item.id,
|
||||
questionId: item.questionId,
|
||||
optionContent: item.optionContent,
|
||||
optionScore: item.optionScore,
|
||||
optionOrder: item.optionOrder,
|
||||
isDefault: item.isDefault || 'N',
|
||||
tenantId: item.tenantId,
|
||||
createBy: item.createBy,
|
||||
createTime: item.createTime,
|
||||
updateBy: item.updateBy,
|
||||
updateTime: item.updateTime,
|
||||
deleted: item.deleted
|
||||
}));
|
||||
|
||||
// 根据optionOrder排序
|
||||
optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
|
||||
} catch (error) {
|
||||
console.error('加载选项失败:', error);
|
||||
optionDialog.options = [];
|
||||
proxy?.$modal.msgError('选项数据加载失败,请重试');
|
||||
} finally {
|
||||
// 关闭加载提示
|
||||
proxy?.$modal.closeLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
optionDialog.options.push({
|
||||
id: undefined,
|
||||
questionId: optionDialog.question?.id,
|
||||
optionContent: '',
|
||||
optionScore: 0,
|
||||
optionOrder: optionDialog.options.length + 1,
|
||||
isDefault: 'N',
|
||||
tenantId: '',
|
||||
createBy: '',
|
||||
createTime: '',
|
||||
updateBy: '',
|
||||
updateTime: '',
|
||||
deleted: '0'
|
||||
});
|
||||
nextTick(() => {
|
||||
// 可选:自动聚焦新行
|
||||
});
|
||||
};
|
||||
|
||||
const addDefaultOptions = () => {
|
||||
// 添加默认的满意度选项
|
||||
const defaultOptions = [
|
||||
{
|
||||
optionContent: '非常满意',
|
||||
optionScore: 5,
|
||||
optionOrder: 0,
|
||||
isDefault: 'Y'
|
||||
},
|
||||
{
|
||||
optionContent: '满意',
|
||||
optionScore: 3,
|
||||
optionOrder: 1,
|
||||
isDefault: 'N'
|
||||
},
|
||||
{
|
||||
optionContent: '一般满意',
|
||||
optionScore: 1,
|
||||
optionOrder: 2,
|
||||
isDefault: 'N'
|
||||
},
|
||||
{
|
||||
optionContent: '不满意',
|
||||
optionScore: 0,
|
||||
optionOrder: 3,
|
||||
isDefault: 'N'
|
||||
}
|
||||
];
|
||||
|
||||
// 将默认选项添加到列表中
|
||||
defaultOptions.forEach((option) => {
|
||||
optionDialog.options.push({
|
||||
id: undefined,
|
||||
questionId: optionDialog.question?.id,
|
||||
optionContent: option.optionContent,
|
||||
optionScore: option.optionScore,
|
||||
optionOrder: option.optionOrder,
|
||||
isDefault: option.isDefault,
|
||||
tenantId: '',
|
||||
createBy: '',
|
||||
createTime: '',
|
||||
updateBy: '',
|
||||
updateTime: '',
|
||||
deleted: '0'
|
||||
});
|
||||
});
|
||||
|
||||
// 提示添加成功
|
||||
proxy?.$modal.msgSuccess('已添加默认选项');
|
||||
};
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
optionDialog.options.splice(index, 1);
|
||||
};
|
||||
|
||||
const saveOptions = async () => {
|
||||
if (!optionDialog.question?.id) {
|
||||
proxy?.$modal.msgError('题目ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复提交
|
||||
if (optionDialog.saveLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置加载状态为true
|
||||
optionDialog.saveLoading = true;
|
||||
|
||||
// 显示加载提示
|
||||
proxy?.$modal.loading('正在保存选项数据...');
|
||||
|
||||
try {
|
||||
// 1. 获取当前题目的所有已有选项
|
||||
const res = await listSatOption({
|
||||
questionId: optionDialog.question.id,
|
||||
pageNum: 1,
|
||||
pageSize: 100
|
||||
});
|
||||
const existingOptions = res.rows || [];
|
||||
|
||||
// 2. 处理选项数据
|
||||
const promises = [];
|
||||
|
||||
// 新增或修改选项
|
||||
for (const option of optionDialog.options) {
|
||||
if (!option.optionContent || option.optionContent.trim() === '') {
|
||||
continue; // 跳过空选项
|
||||
}
|
||||
|
||||
const params = {
|
||||
id: option.id,
|
||||
questionId: optionDialog.question.id,
|
||||
optionContent: option.optionContent,
|
||||
optionScore: option.optionScore || 0,
|
||||
optionOrder: option.optionOrder || 1,
|
||||
status: '0', // 默认启用
|
||||
isDefault: option.isDefault || 'N'
|
||||
};
|
||||
|
||||
if (option.id) {
|
||||
// 已有选项,执行更新
|
||||
promises.push(updateSatOption(params));
|
||||
} else {
|
||||
// 新选项,执行新增
|
||||
promises.push(addSatOption(params));
|
||||
}
|
||||
}
|
||||
|
||||
// 删除不再存在的选项
|
||||
const currentOptionIds = optionDialog.options.filter((opt) => opt.id).map((opt) => opt.id);
|
||||
|
||||
const optionsToDelete = existingOptions.filter((opt) => !currentOptionIds.includes(opt.id)).map((opt) => opt.id);
|
||||
|
||||
if (optionsToDelete.length > 0) {
|
||||
promises.push(delSatOption(optionsToDelete));
|
||||
}
|
||||
|
||||
// 3. 等待所有操作完成
|
||||
await Promise.all(promises);
|
||||
|
||||
// 4. 计算题目分值并更新题目
|
||||
const validOptions = optionDialog.options.filter((opt) => opt.optionContent && opt.optionContent.trim() !== '');
|
||||
let score = 0;
|
||||
|
||||
// 根据题目类型计算分值
|
||||
if (optionDialog.question.questionType === '1') {
|
||||
// 单选题
|
||||
// 单选题的分值是题目选项中最大的分值
|
||||
score = Math.max(...validOptions.map((opt) => Number(opt.optionScore) || 0));
|
||||
} else if (optionDialog.question.questionType === '2') {
|
||||
// 多选题
|
||||
// 多选题的分值是所有题目选项的分值之和
|
||||
score = validOptions.reduce((sum, opt) => sum + (Number(opt.optionScore) || 0), 0);
|
||||
}
|
||||
|
||||
// 更新题目分值
|
||||
if (score > 0) {
|
||||
await updateSatQuestion({
|
||||
id: optionDialog.question.id,
|
||||
categoryId: optionDialog.question.categoryId,
|
||||
questionType: optionDialog.question.questionType,
|
||||
isRequired: optionDialog.question.isRequired,
|
||||
sortOrder: optionDialog.question.sortOrder,
|
||||
status: optionDialog.question.status,
|
||||
remark: optionDialog.question.remark,
|
||||
questionTitle: optionDialog.question.questionTitle,
|
||||
questionContent: optionDialog.question.questionContent,
|
||||
score: score
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 刷新列表
|
||||
await getList();
|
||||
|
||||
// 6. 重新加载选项数据,而不是直接关闭对话框
|
||||
proxy?.$modal.msgSuccess('保存成功');
|
||||
|
||||
// 保存当前选中的题目对象
|
||||
const currentQuestion = { ...optionDialog.question };
|
||||
|
||||
// 重新加载当前题目的选项数据
|
||||
const optRes = await listSatOption({
|
||||
questionId: currentQuestion.id,
|
||||
pageNum: 1,
|
||||
pageSize: 100
|
||||
});
|
||||
|
||||
// 更新选项数据
|
||||
optionDialog.options = optRes.rows.map((item) => ({
|
||||
id: item.id,
|
||||
questionId: item.questionId,
|
||||
optionContent: item.optionContent,
|
||||
optionScore: item.optionScore,
|
||||
optionOrder: item.optionOrder,
|
||||
isDefault: item.isDefault || 'N',
|
||||
tenantId: item.tenantId,
|
||||
createBy: item.createBy,
|
||||
createTime: item.createTime,
|
||||
updateBy: item.updateBy,
|
||||
updateTime: item.updateTime,
|
||||
deleted: item.deleted
|
||||
}));
|
||||
|
||||
// 根据optionOrder排序
|
||||
optionDialog.options.sort((a, b) => a.optionOrder - b.optionOrder);
|
||||
} catch (error) {
|
||||
console.error('保存选项失败:', error);
|
||||
proxy?.$modal.msgError('保存失败,请重试');
|
||||
} finally {
|
||||
// 关闭加载提示
|
||||
proxy?.$modal.closeLoading();
|
||||
// 重置保存按钮状态
|
||||
optionDialog.saveLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getCategoryList();
|
||||
@@ -458,4 +775,9 @@ onMounted(() => {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左边距类 */
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user