提交代码到GIT,修改了满意度调查的问卷

This commit is contained in:
zlwu committed 2025-07-18 09:53:34 +08:00
1 parent c809141195
commit 1c8e15cc0f
43 files changed
+3487 -1606

No files matched your search

@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
/**
@@ -53,5 +53,11 @@ public class HqSatDetailBo extends BaseEntity {
*/
private String remark;
/**
* 题目选项ID
*/
@NotNull(message = "题目选项ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long optionId;
}
@@ -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;
}
@@ -73,5 +73,9 @@ public class HqSatQuestionBo extends BaseEntity {
*/
private String remark;
/**
* 题目分值
*/
private Long score;
}
@@ -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;
}
@@ -123,4 +123,9 @@ public class HqSatSurveyBo extends BaseEntity {
*/
private List<String> statusIn;
/**
* 问卷总分值
*/
private Long score;
}
@@ -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;
}
}
@@ -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;
}
@@ -92,5 +92,11 @@ public class HqSatQuestionVo implements Serializable {
@ExcelProperty(value = "备注")
private String remark;
/**
* 题目分值
*/
@ExcelProperty(value = "题目分值")
private Long score;
}
@@ -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;
}
@@ -119,5 +119,10 @@ public class HqSatSurveyVo implements Serializable {
@ExcelProperty(value = "备注")
private String remark;
/**
* 问卷总分值
*/
@ExcelProperty(value = "问卷总分值")
private Long score;
}
@@ -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);
}
@@ -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 是否进行有效性校验
@@ -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,
@@ -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; }
}
}
+8 -3
View File
@@ -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>
@@ -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);
}
}
}
@@ -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`
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秒
+20 -18
View File
@@ -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是)',
+16 -4
View File
@@ -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;
/**
* 选项详情(单选题和多选题需要)
*/
+58 -28
View File
@@ -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;
/**
* 状态
+10
View File
@@ -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 {
+15
View File
@@ -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;
/**
* 日期范围参数
*/
+320 -247
View File
@@ -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 {
+322
View File
@@ -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