优化问卷回答界面及功能

This commit is contained in:
zlwu committed 2025-06-27 11:47:00 +08:00
1 parent 1ab9f9c458
commit ed5eb9ca52
13 files changed
+6122 -86

No files matched your search

+156 -4
View File
@@ -26,7 +26,8 @@ docs/
│ │ └── README.md # 数据库设计说明
│ └── [模块设计]/ # 其他设计文档
├── development/ # 开发文档
│ └── README.md # 开发指南
│ ├── README.md # 开发指南
│ └── API.md # API接口文档(完整的后端接口说明)
├── testing/ # 测试文档
│ └── README.md # 测试计划和用例
├── deployment/ # 部署文档
@@ -86,6 +87,20 @@ docs/
### ⚠️ 强制要求
**所有通过Cursor AI进行的代码调整都必须在 `docs/code-adjust-record.md` 中留下记录,这是项目管理的强制性要求。**
### 🔴 特别强调:API接口变更的强制性文档更新规则
**⚠️ 超级重要:任何API接口的新增、修改、删除操作都必须立即同步更新 `docs/development/API.md` 文档!**
#### API变更必须执行的步骤:
1. **代码修改完成** → 2. **立即更新API.md文档** → 3. **记录代码调整日志** → 4. **验证文档准确性**
#### API接口变更涉及的文档更新:
- ✅ **新增API接口**:在API.md中添加完整的接口文档(地址、参数、响应、权限等)
- ✅ **修改API接口**:更新API.md中对应接口的参数、响应格式、权限要求
- ✅ **删除API接口**:在API.md中标记接口为已废弃,说明替代方案
- ✅ **修改响应格式**:更新API.md中的响应示例和字段说明
- ✅ **修改权限要求**:更新API.md中的权限要求说明
## 3. 文档同步更新规则
### 📋 核心原则:代码与文档同步更新
@@ -93,7 +108,7 @@ docs/
### 强制更新规则
每次代码调整必须同步更新相关文档:
- **API文档**: 接口变更时更新相关文档
- **API文档**: 接口变更时更新 `docs/development/API.md` 中的相关接口信息
- **数据库文档**: 表结构变更时更新 `docs/design/database/` 下的所有相关文档
- **配置文档**: 配置项变更时更新 `docs/middleware/` 下的配置说明
- **部署文档**: 部署流程变更时更新 `docs/deployment/` 下的部署手册
@@ -110,7 +125,11 @@ docs/
- [ ] **UI设计文档**: 界面样式重大变更时,更新设计规范文档
#### 后端代码调整时需更新的文档:
- [ ] **接口文档**: API接口变更时,更新接口文档和示例
- [ ] **API接口文档**: API接口变更时,必须更新 `docs/development/API.md` 中的相关接口信息,包括:
- 新增接口:添加完整的接口说明、参数、响应示例
- 修改接口:更新参数说明、响应格式、错误码
- 删除接口:在文档中标记为已废弃,并说明替代方案
- 权限变更:更新权限要求说明
- [ ] **数据库文档**: 表结构变更时,更新数据库设计文档
- [ ] **业务逻辑文档**: 业务规则变更时,更新业务流程文档
- [ ] **配置文档**: 系统配置变更时,更新配置说明文档
@@ -121,13 +140,119 @@ docs/
- [ ] **用户手册**: 用户操作流程变更时,更新操作指南
### 🔄 文档更新工作流程
1. **代码调整完成** → 2. **记录代码调整日志** → 3. **识别影响的文档** → 4. **更新相应文档** → 5. **验证文档完整性**
#### 标准流程:
1. **代码调整完成** → 2. **立即更新相关文档** → 3. **记录代码调整日志** → 4. **验证文档完整性**
#### 🔴 API接口变更的特殊流程(强制性):
1. **API代码开发完成**
2. **立即更新 `docs/development/API.md`** (不得延后)
3. **记录代码调整日志,标注API文档已更新**
4. **验证API文档与实际接口的一致性**
5. **提交代码和文档变更**
#### ⚠️ 重要原则:
- **API变更 = 必须更新API.md文档**
- **文档更新 = 代码开发的一部分,不是可选项**
- **未更新文档 = 开发任务未完成**
### 版本管理
- **文档版本**: 与代码版本保持同步
- **变更记录**: 每次变更都要记录变更内容和原因
- **向后兼容**: 接口变更需要考虑向后兼容性
## 4. API文档管理规范 (docs/development/API.md)
### 📄 API文档维护原则
**⚠️ 重要规则:任何后端API接口的新增、修改、删除都必须同步更新 `docs/development/API.md` 文档。**
### API文档更新时机
- **新增Controller**: 添加新的Controller时,必须在API.md中添加对应的接口说明
- **新增接口方法**: 在现有Controller中添加新方法时,更新对应模块的接口列表
- **修改接口参数**: 接口请求或响应参数变更时,更新参数说明表格
- **修改权限要求**: 接口权限变更时,更新权限要求说明
- **修改响应格式**: 响应数据结构变更时,更新响应示例
- **接口废弃**: 接口不再使用时,在文档中标记为已废弃并说明替代方案
### API文档标准格式
每个接口都必须包含以下信息:
```markdown
#### 接口标题
**接口地址**: `HTTP方法 /接口路径`
**接口描述**: 接口功能的详细描述
**权限要求**: 权限代码或"需要登录"
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {}
}
```
### API文档分类规范
- **按业务模块分类**: 问卷管理、答题管理、题目管理等
- **按功能分组**: 基础CRUD操作、业务逻辑操作、状态查询等
- **统一命名规范**: 接口名称要与Controller方法名保持一致
### API文档版本控制
- **版本记录**: 在API.md底部维护版本更新记录
- **变更说明**: 每次更新都要记录具体的变更内容
- **兼容性说明**: 重大变更需要说明向后兼容性影响
### ⚠️ 强制执行规则
**所有通过Cursor AI进行的后端API开发都必须同时更新API文档,这是代码审查的强制性检查项。**
### 🚨 API文档更新检查清单
#### 当进行以下操作时,必须更新 `docs/development/API.md`:
**Controller层变更**:
- [ ] 新增Controller类 → 在API.md中添加新的模块章节
- [ ] 新增@RequestMapping方法 → 添加完整的接口文档
- [ ] 修改@RequestMapping路径 → 更新接口地址
- [ ] 修改HTTP方法(GET/POST/PUT/DELETE) → 更新接口方法
- [ ] 删除接口方法 → 标记为已废弃并说明替代方案
**参数和响应变更**:
- [ ] 新增请求参数 → 更新请求参数表格
- [ ] 修改参数类型或必填属性 → 更新参数说明
- [ ] 修改响应数据结构 → 更新响应示例
- [ ] 新增错误码 → 更新错误码说明表
**权限和安全变更**:
- [ ] 新增@SaCheckPermission注解 → 更新权限要求
- [ ] 修改权限代码 → 更新权限说明
- [ ] 修改接口访问级别 → 更新权限要求说明
#### 强制性文档更新格式要求:
```markdown
#### 接口名称
**接口地址**: `HTTP方法 /完整路径`
**接口描述**: 清晰描述接口功能
**权限要求**: 具体权限代码或"需要登录"
**请求参数**: 完整参数表格
**响应示例**: 真实的JSON响应示例
**错误码**: 可能的错误情况
**更新记录**: 接口变更历史
```
### 🔴 违规检查机制
- **代码审查时检查**:每次PR必须检查API文档是否同步更新
- **自动化提醒**:接口变更后立即更新文档,不得延误
- **文档版本控制**:API文档版本必须与代码版本保持一致
### ⚠️ 重要提醒
**不更新API文档的代码变更将被视为不完整的开发任务,必须补齐文档后才能认为任务完成!**
---
# 第二部分:Web前端开发规范 (web/)
@@ -416,6 +541,33 @@ backend/
@SaCheckPermission("system:user:list")
```
### 🔴 API开发强制性工作流程
#### 步骤1:编写后端代码
- 创建Controller、Service、Mapper等
- 实现业务逻辑和数据处理
- 添加权限控制和参数验证
#### 步骤2:立即更新API文档(强制性)
**⚠️ 代码写完后立即执行,不得延后!**
- 打开 `docs/development/API.md` 文件
- 按照标准格式添加或更新接口文档
- 包含完整的接口信息(地址、参数、响应、权限等)
- 添加真实的请求和响应示例
#### 步骤3:记录代码调整日志
- 在 `docs/code-adjust-record.md` 中记录本次变更
- 特别说明API接口的变更内容
- 标注文档已同步更新
#### 步骤4:验证和测试
- 测试API接口功能是否正常
- 验证API文档的准确性
- 确保文档示例与实际响应一致
#### 🚨 违规后果
**如果发现API变更未同步更新文档,该代码变更将被视为不完整,必须立即补齐文档。**
## 4. 标准代码模板
### Controller模板
@@ -19,8 +19,10 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.zhxy.domain.vo.HqSatSurveyVo;
import org.dromara.zhxy.domain.bo.HqSatSurveyBo;
import org.dromara.zhxy.domain.bo.HqSatAnswerSubmitBo;
import org.dromara.zhxy.service.IHqSatSurveyService;
import org.dromara.zhxy.domain.vo.HqSatSurveyQuestionVo;
import org.dromara.zhxy.domain.vo.HqSatRecordVo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
@@ -130,7 +132,7 @@ public class HqSatSurveyController extends BaseController {
*
* @param surveyId 问卷ID
*/
@SaCheckPermission("hq:satSurvey:question")
// @SaCheckPermission("hq:satSurvey:question")
@GetMapping("/{surveyId}/questions")
public R<List<HqSatSurveyQuestionVo>> getSurveyQuestions(
@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
@@ -177,4 +179,48 @@ public class HqSatSurveyController extends BaseController {
public R<Void> unpublishSurvey(@NotNull(message = "问卷ID不能为空") @PathVariable Long id) {
return toAjax(hqSatSurveyService.unpublishSurvey(id));
}
/**
* 提交问卷答案
*
* @param submitBo 答题提交数据
*/
@Log(title = "提交问卷答案", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/answer/submit")
public R<HqSatRecordVo> submitSurveyAnswer(@Validated @RequestBody HqSatAnswerSubmitBo submitBo) {
return R.ok(hqSatSurveyService.submitSurveyAnswer(submitBo));
}
/**
* 保存问卷答案草稿
*
* @param submitBo 答题提交数据
*/
@Log(title = "保存问卷答案草稿", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/answer/draft")
public R<HqSatRecordVo> saveSurveyDraft(@Validated @RequestBody HqSatAnswerSubmitBo submitBo) {
return R.ok(hqSatSurveyService.saveSurveyDraft(submitBo));
}
/**
* 检查用户是否已答过问卷
*
* @param surveyId 问卷ID
*/
@GetMapping("/{surveyId}/answered")
public R<Boolean> checkUserAnswered(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
return R.ok(hqSatSurveyService.checkUserAnswered(surveyId));
}
/**
* 获取用户问卷答题记录
*
* @param surveyId 问卷ID
*/
@GetMapping("/{surveyId}/answer")
public R<HqSatRecordVo> getUserSurveyAnswer(@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
return R.ok(hqSatSurveyService.getUserSurveyAnswer(surveyId));
}
}
@@ -0,0 +1,106 @@
package org.dromara.zhxy.domain.bo;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
/**
* 答题提交BO
*
* @author wuzilong
* @date 2025-01-26
*/
@Data
public class HqSatAnswerSubmitBo {
/**
* 问卷ID
*/
@NotNull(message = "问卷ID不能为空")
private Long surveyId;
/**
* 答题开始时间
*/
@NotNull(message = "答题开始时间不能为空")
private String startTime;
/**
* 答题结束时间
*/
@NotNull(message = "答题结束时间不能为空")
private String endTime;
/**
* 是否匿名(1是 0否)
*/
@NotNull(message = "是否匿名不能为空")
private Integer isAnonymous;
/**
* 答案列表
*/
@NotEmpty(message = "答案列表不能为空")
private List<AnswerItem> answers;
/**
* 备注
*/
private String remark;
/**
* 单个答案项
*/
@Data
public static class AnswerItem {
/**
* 题目ID
*/
@NotNull(message = "题目ID不能为空")
private Long questionId;
/**
* 题目类型(1单选题 2多选题 3问答题)
*/
@NotNull(message = "题目类型不能为空")
private String questionType;
/**
* 答案值
*/
@NotNull(message = "答案值不能为空")
private String answerValue;
/**
* 选项详情(单选题和多选题需要)
*/
private List<OptionDetail> options;
}
/**
* 选项详情
*/
@Data
public static class OptionDetail {
/**
* 选项ID
*/
private Long optionId;
/**
* 选项值
*/
private String optionValue;
/**
* 选项文本
*/
private String optionText;
/**
* 是否选中
*/
private Boolean isSelected;
}
}
@@ -2,7 +2,9 @@ package org.dromara.zhxy.service;
import org.dromara.zhxy.domain.vo.HqSatSurveyVo;
import org.dromara.zhxy.domain.bo.HqSatSurveyBo;
import org.dromara.zhxy.domain.bo.HqSatAnswerSubmitBo;
import org.dromara.zhxy.domain.vo.HqSatSurveyQuestionVo;
import org.dromara.zhxy.domain.vo.HqSatRecordVo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
@@ -99,4 +101,36 @@ public interface IHqSatSurveyService {
* @return 是否撤销成功
*/
Boolean unpublishSurvey(Long id);
/**
* 提交问卷答案
*
* @param submitBo 答题提交数据
* @return 答题记录
*/
HqSatRecordVo submitSurveyAnswer(HqSatAnswerSubmitBo submitBo);
/**
* 保存问卷答案草稿
*
* @param submitBo 答题提交数据
* @return 答题记录
*/
HqSatRecordVo saveSurveyDraft(HqSatAnswerSubmitBo submitBo);
/**
* 检查用户是否已答过问卷
*
* @param surveyId 问卷ID
* @return 是否已答过
*/
Boolean checkUserAnswered(Long surveyId);
/**
* 获取用户问卷答题记录
*
* @param surveyId 问卷ID
* @return 答题记录
*/
HqSatRecordVo getUserSurveyAnswer(Long surveyId);
}
@@ -12,17 +12,33 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.dromara.zhxy.domain.bo.HqSatSurveyBo;
import org.dromara.zhxy.domain.bo.HqSatAnswerSubmitBo;
import org.dromara.zhxy.domain.bo.HqSatRecordBo;
import org.dromara.zhxy.domain.bo.HqSatDetailBo;
import org.dromara.zhxy.domain.vo.HqSatSurveyVo;
import org.dromara.zhxy.domain.vo.HqSatRecordVo;
import org.dromara.zhxy.domain.HqSatSurvey;
import org.dromara.zhxy.mapper.HqSatSurveyMapper;
import org.dromara.zhxy.service.IHqSatSurveyService;
import org.dromara.zhxy.service.IHqSatSurveyQuestionService;
import org.dromara.zhxy.service.IHqSatRecordService;
import org.dromara.zhxy.service.IHqSatDetailService;
import org.dromara.zhxy.domain.bo.HqSatSurveyQuestionBo;
import org.dromara.zhxy.domain.vo.HqSatSurveyQuestionVo;
import org.dromara.zhxy.domain.vo.HqSatDetailVo;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.core.exception.ServiceException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 问卷Service业务层处理
@@ -37,6 +53,11 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
private final HqSatSurveyMapper baseMapper;
private final IHqSatSurveyQuestionService surveyQuestionService;
private final IHqSatRecordService recordService;
private final IHqSatDetailService detailService;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 查询问卷
@@ -271,4 +292,647 @@ public class HqSatSurveyServiceImpl implements IHqSatSurveyService {
}
return result;
}
/**
* 提交问卷答案
*
* @param submitBo 答题提交数据
* @return 答题记录
*/
@Override
public HqSatRecordVo submitSurveyAnswer(HqSatAnswerSubmitBo submitBo) {
log.info("=== 收到前端答题提交请求 ===");
log.info("问卷ID: {}, 答案数量: {}", submitBo.getSurveyId(),
submitBo.getAnswers() != null ? submitBo.getAnswers().size() : 0);
if (submitBo.getAnswers() != null) {
for (int i = 0; i < submitBo.getAnswers().size(); i++) {
HqSatAnswerSubmitBo.AnswerItem answer = submitBo.getAnswers().get(i);
log.info("答案{}: 题目ID={}, 题目类型={}, 答案值={}, 选项数量={}",
i + 1, answer.getQuestionId(), answer.getQuestionType(), answer.getAnswerValue(),
answer.getOptions() != null ? answer.getOptions().size() : 0);
if (answer.getOptions() != null) {
for (int j = 0; j < answer.getOptions().size(); j++) {
HqSatAnswerSubmitBo.OptionDetail option = answer.getOptions().get(j);
log.info(" 选项{}: ID={}, 值={}, 文本={}, 选中={}",
j + 1, option.getOptionId(), option.getOptionValue(),
option.getOptionText(), option.getIsSelected());
}
}
}
}
return saveAnswerInternal(submitBo, false);
}
/**
* 保存问卷答案草稿
*
* @param submitBo 答题提交数据
* @return 答题记录
*/
@Override
public HqSatRecordVo saveSurveyDraft(HqSatAnswerSubmitBo submitBo) {
return saveAnswerInternal(submitBo, true);
}
/**
* 内部方法:保存答题数据
*
* @param submitBo 答题提交数据
* @param isDraft 是否为草稿
* @return 答题记录
*/
private HqSatRecordVo saveAnswerInternal(HqSatAnswerSubmitBo submitBo, boolean isDraft) {
// 验证问卷是否存在且可以答题
HqSatSurveyVo survey = queryById(submitBo.getSurveyId());
if (survey == null) {
throw new ServiceException("问卷不存在");
}
// 检查问卷状态
if (!isDraft && !"1".equals(survey.getStatus())) {
throw new ServiceException("问卷未在进行中状态,无法提交答案");
}
// 获取当前用户信息
Long userId = LoginHelper.getUserId();
String userTypeString = LoginHelper.getLoginUser().getUserType();
// 将角色字符串转换为数字编码
String userType = convertUserTypeToCode(userTypeString);
// 检查用户提交次数是否超过限制(非草稿模式下)
if (!isDraft) {
int submitCount = getUserSubmitCount(submitBo.getSurveyId(), userId);
int maxSubmitTimes = survey.getMaxSubmitTimes() != null ? survey.getMaxSubmitTimes().intValue() : 1;
log.info("用户提交次数检查: 用户ID={}, 问卷ID={}, 已提交次数={}, 最大允许次数={}",
userId, submitBo.getSurveyId(), submitCount, maxSubmitTimes);
// maxSubmitTimes为0表示不限制提交次数
if (maxSubmitTimes > 0 && submitCount >= maxSubmitTimes) {
throw new ServiceException("您已达到该问卷的最大提交次数限制(" + maxSubmitTimes + "次)");
}
}
// 在保存新答题数据之前,将用户之前的答题记录标记为已删除
markPreviousAnswersAsDeleted(submitBo.getSurveyId(), userId);
// 创建答题记录
HqSatRecordBo recordBo = new HqSatRecordBo();
recordBo.setSurveyId(submitBo.getSurveyId());
recordBo.setUserId(userId);
recordBo.setUserType(userType);
recordBo.setSubmitTime(new Date());
recordBo.setTotalScore(0L); // 暂时设为0,后续可以计算
recordBo.setAvgScore(0L);
recordBo.setRemark(submitBo.getRemark());
// 保存答题记录
boolean recordSaved = recordService.insertByBo(recordBo);
if (!recordSaved) {
throw new ServiceException("保存答题记录失败");
}
// 保存答题详情
for (HqSatAnswerSubmitBo.AnswerItem answerItem : submitBo.getAnswers()) {
HqSatDetailBo detailBo = new HqSatDetailBo();
detailBo.setRecordId(recordBo.getId());
detailBo.setQuestionId(answerItem.getQuestionId());
detailBo.setAnswerScore(0L); // 暂时设为0,后续可以计算分数
// 将答案内容临时存储在remark字段中
String answerContent = buildAnswerContent(answerItem);
detailBo.setRemark(answerContent);
boolean detailSaved = detailService.insertByBo(detailBo);
if (!detailSaved) {
log.error("保存答题详情失败,题目ID: {}", answerItem.getQuestionId());
}
// 保存具体的答案信息到 hq_sat_answer 表
saveAnswerDetails(recordBo.getId(), answerItem);
}
// 返回答题记录
return recordService.queryById(recordBo.getId());
}
/**
* 将用户角色字符串转换为数字编码
*
* @param userTypeString 用户角色字符串
* @return 数字编码字符串
*/
private String convertUserTypeToCode(String userTypeString) {
if (userTypeString == null) {
return "1"; // 默认为学生
}
// 转换为小写进行比较
String lowerCaseType = userTypeString.toLowerCase();
// 学生角色判断
if (lowerCaseType.contains("student") || lowerCaseType.contains("学生") || lowerCaseType.contains("stu")) {
return "1";
}
// 教师角色判断
if (lowerCaseType.contains("teacher") || lowerCaseType.contains("老师") ||
lowerCaseType.contains("教师") || lowerCaseType.contains("教职工") || lowerCaseType.contains("tea")) {
return "2";
}
// 管理员角色判断
if (lowerCaseType.contains("admin") || lowerCaseType.contains("管理员") ||
lowerCaseType.contains("超级管理员") || lowerCaseType.contains("administrator")) {
return "3";
}
// 默认返回学生
log.warn("未识别的用户角色类型: {}, 默认设置为学生角色", userTypeString);
return "1";
}
/**
* 构建答案内容字符串
*
* @param answerItem 答案项
* @return 答案内容字符串
*/
private String buildAnswerContent(HqSatAnswerSubmitBo.AnswerItem answerItem) {
StringBuilder content = new StringBuilder();
content.append("题目类型:").append(answerItem.getQuestionType()).append(";");
content.append("答案值:").append(answerItem.getAnswerValue()).append(";");
if (answerItem.getOptions() != null && !answerItem.getOptions().isEmpty()) {
content.append("选项详情:");
for (HqSatAnswerSubmitBo.OptionDetail option : answerItem.getOptions()) {
if (Boolean.TRUE.equals(option.getIsSelected())) {
content.append(option.getOptionValue()).append("(")
.append(option.getOptionText()).append("),");
}
}
}
return content.toString();
}
/**
* 将用户之前的答题记录标记为已删除
*
* @param surveyId 问卷ID
* @param userId 用户ID
*/
private void markPreviousAnswersAsDeleted(Long surveyId, Long userId) {
try {
log.info("=== 开始标记用户之前的答题记录为已删除 ===");
log.info("问卷ID: {}, 用户ID: {}", surveyId, userId);
// 1. 查找该用户对该问卷的所有历史答题记录ID
String findRecordsSql = """
SELECT id FROM hq_sat_record
WHERE survey_id = ? AND user_id = ? AND deleted = 0
""";
List<Map<String, Object>> recordResults = jdbcTemplate.queryForList(findRecordsSql, surveyId, userId);
if (recordResults.isEmpty()) {
log.info("用户没有历史答题记录,无需标记删除");
return;
}
log.info("找到 {} 条历史答题记录", recordResults.size());
// 2. 标记答题记录表为已删除
String updateRecordSql = """
UPDATE hq_sat_record
SET deleted = 1, update_by = ?, update_time = NOW()
WHERE survey_id = ? AND user_id = ? AND deleted = 0
""";
int recordUpdated = jdbcTemplate.update(updateRecordSql,
LoginHelper.getUserId(), surveyId, userId);
log.info("标记答题记录为已删除,影响行数: {}", recordUpdated);
// 3. 标记答题详情表为已删除(通过record_id关联)
for (Map<String, Object> record : recordResults) {
Long recordId = Long.valueOf(record.get("id").toString());
String updateDetailSql = """
UPDATE hq_sat_detail
SET deleted = 1, update_by = ?, update_time = NOW()
WHERE record_id = ? AND deleted = 0
""";
int detailUpdated = jdbcTemplate.update(updateDetailSql,
LoginHelper.getUserId(), recordId);
log.info("标记答题详情为已删除,记录ID: {}, 影响行数: {}", recordId, detailUpdated);
// 4. 标记答案表为已删除(通过record_id关联)
String updateAnswerSql = """
UPDATE hq_sat_answer
SET deleted = 1, update_by = ?, update_time = NOW()
WHERE record_id = ? AND deleted = 0
""";
int answerUpdated = jdbcTemplate.update(updateAnswerSql,
LoginHelper.getUserId(), recordId);
log.info("标记答案数据为已删除,记录ID: {}, 影响行数: {}", recordId, answerUpdated);
}
log.info("=== 用户历史答题记录标记删除完成 ===");
} catch (Exception e) {
log.error("标记用户历史答题记录为已删除失败,问卷ID: {}, 用户ID: {}, 错误详情: {}",
surveyId, userId, e.getMessage(), e);
// 这里不抛出异常,避免影响新答题数据的保存
log.warn("历史数据删除失败不影响新答题数据保存,继续执行后续流程");
}
}
/**
* 检查用户是否已答过问卷
*
* @param surveyId 问卷ID
* @return 是否已答过
*/
@Override
public Boolean checkUserAnswered(Long surveyId) {
Long userId = LoginHelper.getUserId();
return getUserSubmitCount(surveyId, userId) > 0;
}
/**
* 获取用户对指定问卷的提交次数(只统计未删除的记录)
*
* @param surveyId 问卷ID
* @param userId 用户ID
* @return 提交次数
*/
private int getUserSubmitCount(Long surveyId, Long userId) {
try {
// 使用SQL直接查询,确保只统计未删除的记录
String countSql = """
SELECT COUNT(*) FROM hq_sat_record
WHERE survey_id = ? AND user_id = ? AND deleted = 0
""";
Integer count = jdbcTemplate.queryForObject(countSql, Integer.class, surveyId, userId);
int submitCount = count != null ? count : 0;
log.debug("用户{}对问卷{}的有效提交次数: {}", userId, surveyId, submitCount);
return submitCount;
} catch (Exception e) {
log.error("查询用户提交次数失败,问卷ID: {}, 用户ID: {}, 错误: {}",
surveyId, userId, e.getMessage(), e);
// 发生异常时使用原有方式查询作为降级方案
log.warn("使用降级方案查询提交次数");
HqSatRecordBo queryBo = new HqSatRecordBo();
queryBo.setSurveyId(surveyId);
queryBo.setUserId(userId);
List<HqSatRecordVo> records = recordService.queryList(queryBo);
int count = records.size();
log.debug("降级方案统计用户{}对问卷{}的提交次数: {}", userId, surveyId, count);
return count;
}
}
/**
* 获取用户问卷答题记录(只返回未删除的最新记录)
*
* @param surveyId 问卷ID
* @return 答题记录(包含答题详情)
*/
@Override
public HqSatRecordVo getUserSurveyAnswer(Long surveyId) {
Long userId = LoginHelper.getUserId();
HqSatRecordVo record = null;
try {
// 使用SQL直接查询,确保只获取未删除的最新记录
String findLatestRecordSql = """
SELECT * FROM hq_sat_record
WHERE survey_id = ? AND user_id = ? AND deleted = 0
ORDER BY create_time DESC
LIMIT 1
""";
List<Map<String, Object>> recordResults = jdbcTemplate.queryForList(findLatestRecordSql, surveyId, userId);
if (recordResults.isEmpty()) {
throw new ServiceException("未找到有效的答题记录", 404);
}
Map<String, Object> recordMap = recordResults.get(0);
Long recordId = Long.valueOf(recordMap.get("id").toString());
log.info("找到用户最新有效答题记录,记录ID: {}", recordId);
// 通过recordId获取完整的记录对象
record = recordService.queryById(recordId);
if (record == null) {
throw new ServiceException("答题记录数据异常", 500);
}
} catch (ServiceException e) {
throw e; // 重新抛出业务异常
} catch (Exception e) {
log.error("查询用户答题记录失败,使用降级方案,问卷ID: {}, 用户ID: {}, 错误: {}",
surveyId, userId, e.getMessage(), e);
// 降级方案:使用原有方式查询
HqSatRecordBo queryBo = new HqSatRecordBo();
queryBo.setSurveyId(surveyId);
queryBo.setUserId(userId);
List<HqSatRecordVo> records = recordService.queryList(queryBo);
if (records.isEmpty()) {
throw new ServiceException("未找到答题记录", 404);
}
// 获取最新的一条记录
record = records.get(0);
}
// 直接从 hq_sat_answer 表查询答案详情(更准确的数据源)
List<Map<String, Object>> answers = getAnswersFromSatAnswerTable(record.getId());
log.debug("从 hq_sat_answer 表查询到答案记录数: {}", answers.size());
// 构造包含答案详情的记录对象
HqSatRecordVo recordWithAnswers = new HqSatRecordVo();
recordWithAnswers.setId(record.getId());
recordWithAnswers.setSurveyId(record.getSurveyId());
recordWithAnswers.setUserId(record.getUserId());
recordWithAnswers.setUserType(record.getUserType());
recordWithAnswers.setUserDept(record.getUserDept());
recordWithAnswers.setSubmitTime(record.getSubmitTime());
recordWithAnswers.setTotalScore(record.getTotalScore());
recordWithAnswers.setAvgScore(record.getAvgScore());
recordWithAnswers.setRemark(record.getRemark());
// 将answers数组作为扩展属性设置到记录对象
// 由于HqSatRecordVo没有answers字段,我们使用一个包装类
ExtendedSatRecordVo extendedRecord = new ExtendedSatRecordVo();
extendedRecord.setRecord(recordWithAnswers);
extendedRecord.setAnswers(answers);
// 临时使用remark字段传递answers数据
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
String answersJson = mapper.writeValueAsString(answers);
recordWithAnswers.setRemark("ANSWERS_DATA:" + answersJson);
log.debug("答案数据序列化成功,数据长度: {}", answersJson.length());
} catch (Exception e) {
log.error("序列化答案数据失败", e);
}
return recordWithAnswers;
}
/**
* 从 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
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);
}
// 转换为列表并返回
List<Map<String, Object>> result = new ArrayList<>(questionAnswerMap.values());
log.debug("聚合后的答案数据: {} 个题目", result.size());
return result;
} catch (Exception e) {
log.error("从 hq_sat_answer 表查询答案失败,记录ID: {}, 错误: {}",
recordId, e.getMessage(), e);
return new ArrayList<>();
}
}
/**
* 从remark字段解析答案数据
*
* @param detail 答题详情
* @return 解析后的答案数据
*/
private Map<String, Object> parseAnswerFromRemark(HqSatDetailVo detail) {
if (detail.getRemark() == null || detail.getRemark().trim().isEmpty()) {
return null;
}
Map<String, Object> answer = new HashMap<>();
answer.put("questionId", detail.getQuestionId());
String remark = detail.getRemark();
log.debug("解析答题详情,题目ID: {}, remark: {}", detail.getQuestionId(), remark);
// 解析remark中的数据格式: "题目类型:1;答案值:A;选项详情:A(选项文本),"
try {
String[] parts = remark.split(";");
String questionType = "";
String answerValue = "";
for (String part : parts) {
if (part.startsWith("题目类型:")) {
questionType = part.substring("题目类型:".length());
answer.put("questionType", questionType);
} else if (part.startsWith("答案值:")) {
answerValue = part.substring("答案值:".length());
answer.put("answerValue", answerValue);
}
}
// 设置其他默认值
answer.put("id", detail.getId());
answer.put("recordId", detail.getRecordId());
answer.put("answerScore", detail.getAnswerScore() != null ? detail.getAnswerScore() : 0);
answer.put("remark", detail.getRemark());
log.debug("解析答案完成: questionId={}, questionType={}, answerValue={}",
detail.getQuestionId(), questionType, answerValue);
return answer;
} catch (Exception e) {
log.error("解析答题详情失败,题目ID: {}, remark: {}", detail.getQuestionId(), remark, e);
return null;
}
}
/**
* 保存答案详情到 hq_sat_answer 表
*
* @param recordId 答题记录ID
* @param answerItem 答案项
*/
private void saveAnswerDetails(Long recordId, HqSatAnswerSubmitBo.AnswerItem answerItem) {
try {
String questionType = answerItem.getQuestionType();
String answerValue = answerItem.getAnswerValue();
log.info("=== 开始保存答案详情到 hq_sat_answer 表 ===");
log.info("记录ID: {}, 题目ID: {}, 题目类型: {}, 答案值: {}",
recordId, answerItem.getQuestionId(), questionType, answerValue);
log.info("选项数据: {}", answerItem.getOptions());
if (questionType.equals("1") || questionType.equals("2")) {
// 单选题或多选题:保存选项信息
log.info("处理单选题或多选题,题目类型: {}", questionType);
if (answerItem.getOptions() != null && !answerItem.getOptions().isEmpty()) {
log.info("选项数据不为空,选项数量: {}", answerItem.getOptions().size());
for (HqSatAnswerSubmitBo.OptionDetail option : answerItem.getOptions()) {
log.info("处理选项: optionId={}, optionValue={}, optionText={}, isSelected={}",
option.getOptionId(), option.getOptionValue(), option.getOptionText(),
option.getIsSelected());
if (Boolean.TRUE.equals(option.getIsSelected())) {
log.info("选项已选中,准备插入数据库");
// 根据选项ID查询真实的分值
Long realScore = 0L;
try {
String scoreSql = "SELECT option_value 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");
realScore = scoreValue != null ? Long.valueOf(scoreValue.toString()) : 0L;
log.info("查询到选项{}的真实分值: {}", option.getOptionId(), realScore);
}
} catch (Exception e) {
log.warn("查询选项分值失败,使用默认值0,选项ID: {}, 错误: {}", option.getOptionId(), e.getMessage());
}
String sql = """
INSERT INTO hq_sat_answer (
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)
""";
int rowsAffected = jdbcTemplate.update(sql,
LoginHelper.getTenantId(), // tenant_id
LoginHelper.getUserId(), // create_by
recordId, // record_id
answerItem.getQuestionId(), // question_id
questionType, // question_type
option.getOptionId(), // option_id
option.getOptionValue(), // option_value(A、B、C、D)
option.getOptionText(), // option_text
answerValue, // answer_value
realScore // answer_score(真实分值)
);
log.info("选择题答案插入成功,影响行数: {}, 选项标识: {}, 分值: {}",
rowsAffected, option.getOptionValue(), realScore);
} else {
log.info("选项未选中,跳过: {}", option.getOptionValue());
}
}
} else {
log.warn("选择题选项数据为空或null,题目ID: {}, 题目类型: {}",
answerItem.getQuestionId(), questionType);
}
} else if (questionType.equals("3")) {
// 问答题:直接保存文本内容
log.info("处理问答题,答案值: {}", answerValue);
String sql = """
INSERT INTO hq_sat_answer (
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)
""";
int rowsAffected = jdbcTemplate.update(sql,
LoginHelper.getTenantId(), // tenant_id
LoginHelper.getUserId(), // create_by
recordId, // record_id
answerItem.getQuestionId(), // question_id
questionType, // question_type
answerValue // answer_value
);
log.info("问答题答案插入成功,影响行数: {}", rowsAffected);
}
log.info("=== 答案详情保存完成 ===");
} catch (Exception e) {
log.error("保存答案到 hq_sat_answer 表失败,题目ID: {}, 题目类型: {}, 错误详情: {}",
answerItem.getQuestionId(), answerItem.getQuestionType(), e.getMessage(), e);
}
}
/**
* 扩展的答题记录VO,包含答案列表
*/
private static class ExtendedSatRecordVo {
private HqSatRecordVo record;
private List<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;
}
}
}
File diff suppressed because it is too large. Load diff
+771
View File
@@ -0,0 +1,771 @@
# API 接口文档
## 文档说明
本文档记录项目所有的 API 接口信息,包括请求方式、参数说明、响应格式等。所有接口遵循 RESTful 设计规范。
### 基础信息
- **基础路径**: `/api`
- **响应格式**: JSON
- **字符编码**: UTF-8
- **权限认证**: Sa-Token + JWT
- **版本**: v1.0
### 统一响应格式
```json
{
"code": 200,
"msg": "操作成功",
"data": {}
}
```
#### 状态码说明
| 状态码 | 说明 | 备注 |
| ------ | ---------- | ---------------------- |
| 200 | 成功 | 请求成功 |
| 400 | 参数错误 | 请求参数格式或内容错误 |
| 401 | 未认证 | 需要登录 |
| 403 | 无权限 | 无访问权限 |
| 404 | 不存在 | 请求的资源不存在 |
| 500 | 服务器错误 | 内部服务器错误 |
---
## 1. 问卷管理 API
### 1.1 问卷基础管理
#### 查询问卷列表
**接口地址**: `GET /hq/satSurvey/list`
**接口描述**: 分页查询问卷列表,支持权限过滤
**权限要求**: `hq:satSurvey:list`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | ---- | --------------------- |
| pageNum | int | 否 | 页码,默认 1 |
| pageSize | int | 否 | 页大小,默认 10 |
| surveyCode | string | 否 | 问卷编号 |
| surveyTitle | string | 否 | 问卷标题 |
| targetGroup | string | 否 | 目标群体 |
| status | string | 否 | 问卷状态 |
| isPublished | int | 否 | 是否发布(1 是 0 否) |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"total": 100,
"rows": [
{
"id": 1,
"surveyCode": "SURVEY001",
"surveyTitle": "满意度调查",
"surveyDesc": "学生满意度调查",
"targetGroup": "1",
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-01-31 23:59:59",
"isAnonymous": 1,
"isPublished": 1,
"status": "1"
}
]
}
}
```
#### 获取问卷详情
**接口地址**: `GET /hq/satSurvey/{id}`
**接口描述**: 根据 ID 获取问卷详细信息
**权限要求**: `hq:satSurvey:query`
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------ | ---- | ---- | ------- |
| id | long | 是 | 问卷 ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1,
"surveyCode": "SURVEY001",
"surveyTitle": "满意度调查",
"surveyDesc": "学生满意度调查",
"surveyInstruction": "请认真填写",
"targetGroup": "1",
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-01-31 23:59:59",
"maxSubmitTimes": 1,
"isAnonymous": 1,
"isPublished": 1,
"status": "1",
"remark": ""
}
}
```
#### 新增问卷
**接口地址**: `POST /hq/satSurvey`
**接口描述**: 创建新的问卷
**权限要求**: `hq:satSurvey:add`
**请求参数**:
```json
{
"surveyCode": "SURVEY001",
"surveyTitle": "满意度调查",
"surveyDesc": "学生满意度调查",
"surveyInstruction": "请认真填写",
"targetGroup": "1",
"startTime": "2025-01-01 00:00:00",
"endTime": "2025-01-31 23:59:59",
"maxSubmitTimes": 1,
"isAnonymous": 1,
"remark": ""
}
```
#### 修改问卷
**接口地址**: `PUT /hq/satSurvey`
**接口描述**: 修改问卷信息
**权限要求**: `hq:satSurvey:edit`
#### 删除问卷
**接口地址**: `DELETE /hq/satSurvey/{ids}`
**接口描述**: 批量删除问卷
**权限要求**: `hq:satSurvey:remove`
#### 发布问卷
**接口地址**: `PUT /hq/satSurvey/{id}/publish`
**接口描述**: 发布问卷,将状态改为进行中
**权限要求**: `hq:satSurvey:publish`
#### 撤销发布问卷
**接口地址**: `PUT /hq/satSurvey/{id}/unpublish`
**接口描述**: 撤销发布问卷,将状态改为草稿
**权限要求**: `hq:satSurvey:unpublish`
### 1.2 问卷题目管理
#### 获取问卷题目列表
**接口地址**: `GET /hq/satSurvey/{surveyId}/questions`
**接口描述**: 获取问卷关联的题目列表,包含题目详细信息和选项
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ---- | ---- | ------- |
| surveyId | long | 是 | 问卷 ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"surveyId": 1,
"questionId": 1,
"sortOrder": 1,
"weight": 1,
"isRequired": 1,
"questionTitle": "您对服务质量的评价?",
"questionType": "1",
"questionDesc": "请选择一个选项",
"options": [
{
"id": 1,
"questionId": 1,
"optionValue": "A",
"optionText": "非常满意",
"optionScore": 5,
"sortOrder": 1
}
]
}
]
}
```
#### 更新问卷题目关联
**接口地址**: `PUT /hq/satSurvey/{surveyId}/questions`
**接口描述**: 批量更新问卷题目关联关系
**权限要求**: `hq:satSurvey:question`
---
## 2. 答题 API(新增)
### 2.1 答题提交
#### 提交问卷答案
**接口地址**: `POST /hq/satSurvey/answer/submit`
**接口描述**: 提交问卷答案,正式提交后不可修改
**权限要求**: 需要登录
**请求参数**:
```json
{
"surveyId": 1,
"startTime": "2025-01-26T14:00:00.000Z",
"endTime": "2025-01-26T14:15:00.000Z",
"isAnonymous": 1,
"answers": [
{
"questionId": 1,
"questionType": "1",
"answerValue": "A",
"options": [
{
"optionId": 1,
"optionValue": "A",
"optionText": "非常满意",
"isSelected": true
}
]
},
{
"questionId": 2,
"questionType": "2",
"answerValue": "A,C,D",
"options": [
{
"optionId": 2,
"optionValue": "A",
"optionText": "选项A",
"isSelected": true
},
{
"optionId": 4,
"optionValue": "C",
"optionText": "选项C",
"isSelected": true
}
]
},
{
"questionId": 3,
"questionType": "3",
"answerValue": "这是我的意见和建议内容",
"options": []
}
],
"remark": ""
}
```
**参数说明**:
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | ---- | ----------------------------- |
| surveyId | long | 是 | 问卷 ID |
| startTime | string | 是 | 答题开始时间(ISO 8601 格式) |
| endTime | string | 是 | 答题结束时间(ISO 8601 格式) |
| isAnonymous | int | 是 | 是否匿名(1 是 0 否) |
| answers | array | 是 | 答案列表 |
| remark | string | 否 | 备注 |
**answers 中每项参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ------------ | ------ | ---- | ------------------------------------------------ |
| questionId | long | 是 | 题目 ID |
| questionType | string | 是 | 题目类型(1 单选题 2 多选题 3 问答题) |
| answerValue | string | 是 | 答案值(单选存选项值,多选逗号分隔,问答存文本) |
| options | array | 否 | 选项详情(单选题和多选题需要) |
**响应示例**:
```json
{
"code": 200,
"msg": "答题提交成功",
"data": {
"id": 1001,
"surveyId": 1,
"userId": 1,
"userType": "student",
"submitTime": "2025-01-26T14:15:00.000Z",
"totalScore": 0,
"avgScore": 0,
"remark": ""
}
}
```
**多次提交功能说明**(v1.0.3 新增,v1.0.4 优化):
系统支持问卷多次提交功能,提交次数限制由问卷的 `max_submit_times` 字段控制:
1. **提交次数规则**:
- `max_submit_times = 0`: 不限制提交次数,用户可以无限次提交
- `max_submit_times = 1`: 最多提交 1 次(默认行为,向后兼容)
- `max_submit_times = N`: 最多提交 N 次
2. **数据存储策略**(v1.0.4 重要更新):
- **软删除历史数据**: 用户提交新答案时,系统会自动将之前的答题记录标记为已删除(deleted=1)
- **保持数据完整性**: 历史数据完整保留用于审计,但业务查询只返回最新有效数据
- **关联删除**: 通过 record_id 关联,确保 hq_sat_record、hq_sat_detail、hq_sat_answer 三表数据状态同步
3. **数据状态管理**:
- `deleted = 0`: 有效数据,正常业务查询可见
- `deleted = 1`: 历史数据,仅审计查询可见
- 用户每次只能看到最新的有效答题记录
4. **处理流程**:
```
用户提交新答案 → 标记历史数据为删除 → 保存新答题数据 → 返回最新记录
```
5. **错误处理**:
```json
{
"code": 500,
"msg": "您已达到该问卷的最大提交次数限制(3次)",
"data": null
}
```
6. **使用场景**:
- **单次提交**: 正式考试、重要调研等
- **多次提交**: 练习测试、阶段性评估等
- **无限提交**: 学习练习、反馈收集等
7. **业务优势**:
- ✅ **用户体验**: 查看答题记录始终是最新版本
- ✅ **数据审计**: 历史记录完整保留,支持数据回溯
- ✅ **查询性能**: 业务查询只涉及有效数据,提高效率
- ✅ **数据一致性**: 避免多版本数据混乱问题
**数据保存说明**:
此接口在保存答题记录时,会同时保存详细的答案信息到两个表:
1. **hq_sat_detail 表**:保存答题概要信息
- 记录答题记录 ID、题目 ID、答案分数等基础信息
- remark 字段存储答案内容的摘要信息
2. **hq_sat_answer 表**:保存具体的答案详情(v1.0.1 新增,v1.0.2 修复)
- **单选题**:为选中的选项创建一条记录,包含选项 ID、选项标识(A/B/C/D)、选项文本、真实分值
- **多选题**:为每个选中的选项创建一条记录,使用选项标识区分不同选项
- **问答题**:创建一条记录,选项相关字段为 NULL,答案值存储用户输入的文本内容
**hq_sat_answer 表数据示例**(v1.0.2 修复后):
```sql
-- 单选题答案记录(用户选择"非常满意"选项)
INSERT INTO hq_sat_answer VALUES (
1, '000000', 1001, NOW(), 1, NOW(), 0, -- 基础字段
123, 1, '1', 1, 'A', '非常满意', 'A', 5, NULL -- 答案字段
);
-- 多选题答案记录(用户选中A、C两个选项)
INSERT INTO hq_sat_answer VALUES
(2, '000000', 1001, NOW(), 1, NOW(), 0, 123, 2, '2', 1, 'A', '非常满意', 'A,C', 5, NULL),
(3, '000000', 1001, NOW(), 1, NOW(), 0, 123, 2, '2', 3, 'C', '一般', 'A,C', 1, NULL);
-- 问答题答案记录
INSERT INTO hq_sat_answer VALUES (
4, '000000', 1001, NOW(), 1, NOW(), 0, -- 基础字段
123, 3, '3', NULL, NULL, NULL, '这是我的意见和建议', 0, NULL -- 答案字段
);
```
**字段说明**(修复后):
- `option_value`: 存储选项标识(A、B、C、D),便于统计分析
- `answer_score`: 存储真实分值(从 hq_sat_option 表查询获得)
- `answer_value`: 单选题存选项标识,多选题存逗号分隔的多个标识,问答题存文本
#### 保存问卷答案草稿
**接口地址**: `POST /hq/satSurvey/answer/draft`
**接口描述**: 保存问卷答案草稿,可多次保存和修改
**权限要求**: 需要登录
**请求参数**: 同提交问卷答案
**响应示例**: 同提交问卷答案
### 2.2 答题状态查询
#### 检查用户是否已答过问卷
**接口地址**: `GET /hq/satSurvey/{surveyId}/answered`
**接口描述**: 检查当前用户是否已经答过指定问卷(v1.0.3 更新:支持多次提交检查)
**权限要求**: 需要登录
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ---- | ---- | ------- |
| surveyId | long | 是 | 问卷 ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": true
}
```
**响应说明**(v1.0.3 更新):
- `data` 为 `true` 表示用户已答过该问卷(至少提交过 1 次)
- `data` 为 `false` 表示用户未答过该问卷
**多次提交支持**:
此接口仅返回用户是否有答题记录,不考虑提交次数限制。要检查是否还能继续提交,请参考以下逻辑:
- 如果 `max_submit_times = 0`:始终可以提交
- 如果 `max_submit_times > 0`:需要检查用户提交次数是否小于此限制
- 具体的提交限制检查由"提交问卷答案"接口在提交时进行
#### 获取用户问卷答题记录
**接口地址**: `GET /hq/satSurvey/{surveyId}/answer`
**接口描述**: 获取当前用户对指定问卷的答题记录(v1.0.5 重大更新:优化数据源,支持前端答案回显)
**权限要求**: 需要登录
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| -------- | ---- | ---- | ------- |
| surveyId | long | 是 | 问卷 ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1001,
"surveyId": 1,
"userId": 1,
"userType": "student",
"submitTime": "2025-01-26T14:15:00.000Z",
"totalScore": 0,
"avgScore": 0,
"remark": "ANSWERS_DATA:[{\"questionId\":1,\"questionType\":\"1\",\"answerValue\":\"A\"},{\"questionId\":2,\"questionType\":\"2\",\"answerValue\":\"A,C\"}]"
}
}
```
**返回数据说明**(v1.0.5 重大更新):
- 此接口只返回用户最新的有效答题记录(deleted=0)
- 如果用户进行了多次提交,历史记录已被软删除,查询不到
- 返回的是用户最后一次提交的完整答题信息
- 历史记录完整保留在数据库中,仅用于审计目的
**v1.0.5 新增功能 - 答案回显数据**:
为支持前端答题界面的历史答案回显功能,此接口现在会在 `remark` 字段中包含格式化的答案数据:
1. **数据格式**: `ANSWERS_DATA:[答案JSON数组]`
2. **数据来源**: 直接从 `hq_sat_answer` 表查询,比 v1.0.4 之前的 `remark` 解析更准确
3. **答案结构**:
```json
[
{
"questionId": 1,
"questionType": "1",
"answerValue": "A"
},
{
"questionId": 2,
"questionType": "2",
"answerValue": "A,C"
},
{
"questionId": 3,
"questionType": "3",
"answerValue": "用户输入的文本内容"
}
]
```
**前端答案回显逻辑**:
- **单选题** (`questionType = "1"`): `answerValue` 直接设置到单选控件(如 "A")
- **多选题** (`questionType = "2"`): `answerValue` 按逗号分割成数组(如 ["A", "C"])
- **问答题** (`questionType = "3"`): `answerValue` 直接设置到文本框
**使用场景**:
- ✅ **首次答题**: 返回 404,前端正常显示空表单
- ✅ **重新答题**: 返回历史答案,前端自动回显到表单
- ✅ **多次修改**: 基于上次答案继续编辑,提升用户体验
---
## 3. 题目管理 API
### 3.1 题目基础管理
#### 查询题目列表
**接口地址**: `GET /hq/satQuestion/list`
**权限要求**: `hq:satQuestion:list`
#### 获取题目选项
**接口地址**: `GET /hq/satQuestion/{questionId}/options`
**接口描述**: 获取指定题目的所有选项
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
| ---------- | ---- | ---- | ------- |
| questionId | long | 是 | 题目 ID |
**响应示例**:
```json
{
"code": 200,
"msg": "查询成功",
"data": [
{
"id": 1,
"questionId": 1,
"optionValue": "A",
"optionText": "非常满意",
"optionScore": 5,
"sortOrder": 1
},
{
"id": 2,
"questionId": 1,
"optionValue": "B",
"optionText": "满意",
"optionScore": 4,
"sortOrder": 2
}
]
}
```
---
## 4. 答题记录管理 API
### 4.1 答题记录查询
#### 查询答题记录列表
**接口地址**: `GET /hq/satRecord/list`
**权限要求**: `hq:satRecord:list`
#### 获取答题记录详情
**接口地址**: `GET /hq/satRecord/{id}`
**权限要求**: `hq:satRecord:query`
---
## 5. 错误码说明
### 业务错误码
| 错误码 | 说明 | 解决方案 |
| ------ | ------------------ | ------------------------ |
| 40001 | 问卷不存在 | 检查问卷 ID 是否正确 |
| 40002 | 问卷未在进行中状态 | 等待问卷开始或联系管理员 |
| 40003 | 您已经答过这份问卷 | 不能重复答题 |
| 40004 | 答题记录保存失败 | 重新提交或联系技术支持 |
| 40005 | 题目类型不支持 | 检查题目类型是否正确 |
---
## 6. 数据字典
### 6.1 问卷状态
| 值 | 说明 | 备注 |
| --- | ------ | -------------- |
| 0 | 草稿 | 未发布状态 |
| 1 | 进行中 | 已发布,可答题 |
| 2 | 已结束 | 超过结束时间 |
### 6.2 目标群体
| 值 | 说明 | 备注 |
| --- | ------ | ---------- |
| 1 | 学生 | 在校学生 |
| 2 | 教职工 | 教师和职工 |
| 3 | 全体 | 所有人员 |
### 6.4 用户类型
| 值 | 说明 | 备注 |
| --- | ---------- | --------------------------------------------------------- |
| 1 | 学生 | student、学生、stu 等角色映射为此值 |
| 2 | 教师 | teacher、老师、教师、教职工、tea 等角色映射为此值 |
| 3 | 超级管理员 | admin、管理员、超级管理员、administrator 等角色映射为此值 |
### 6.3 题目类型
| 值 | 说明 | 备注 |
| --- | ------ | ---------------- |
| 1 | 单选题 | 只能选择一个选项 |
| 2 | 多选题 | 可以选择多个选项 |
| 3 | 问答题 | 文本输入 |
---
## 7. 版本更新记录
### v1.0.5 (2025-01-26)
- 🚀 **重大更新**:实现答题界面历史答案回显功能,提升多次答题用户体验
- 🔧 **数据源优化**:`GET /hq/satSurvey/{surveyId}/answer` 接口改用 hq_sat_answer 表直接查询
- 📊 新增 `getAnswersFromSatAnswerTable()` 方法,提供更准确的答案数据聚合
- ✨ 前端新增 `loadPreviousAnswers()` 方法,支持三种题型的智能答案回显
- 🎯 智能用户体验:只在成功加载历史答案时显示提示,异常时优雅降级
- 🛡️ 错误处理优化:404 错误不显示提示,历史答案加载失败不阻断答题流程
### v1.0.4 (2025-01-26)
- 🔄 **重要优化**:多次提交时实现历史数据软删除,确保用户只有一份有效答题记录
- 🗃️ 保留历史数据用于审计,但业务查询只返回最新有效数据
- ⚡ 优化查询性能,提交次数统计和答题记录查询只涉及有效数据
- 🛡️ 增强数据一致性,通过 record_id 关联确保三表数据状态同步
### v1.0.3 (2025-01-26)
- 🚀 **新功能**:实现问卷多次提交功能,支持根据 max_submit_times 设置控制提交次数
- 📊 优化提交次数检查逻辑,支持 0=无限制、N=最大 N 次的灵活配置
- ✅ 保留完整的答题历史记录,每次提交创建新的答题记录
- 🎯 增强用户体验,提供详细的提交次数限制提示信息
### v1.0.2 (2025-01-26)
- 🔧 **重要修复**:修复单选题和多选题答案未记录到 hq_sat_answer 表的问题
- 🎯 修复前端选项绑定使用分值导致的数据类型不匹配问题
- ✅ 优化答案保存逻辑,正确存储选项标识(A、B、C、D)和分值
- 📊 增强答案数据查询,支持选项分值自动计算
- 🐛 修复多选题答案保存逻辑错误
### v1.0.1 (2025-01-26)
- 🔧 修复答题提交时 user_type 字段长度问题
- ✅ 新增用户角色到数字编码的转换逻辑
- 📚 新增用户类型数据字典说明
- 🆕 新增答案详情记录功能,同时保存到 hq_sat_answer 表
- 📝 完善提交问卷答案接口的数据保存说明
### v1.0.0 (2025-01-26)
- ✅ 新增答题提交 API
- ✅ 新增答题状态查询 API
- ✅ 完善问卷管理 API
- ✅ 新增题目选项查询 API
- ✅ 建立完整的 API 文档体系
---
## 8. 开发说明
### 8.1 接口测试
推荐使用以下工具进行接口测试:
- **Postman**: 图形化接口测试工具
- **Apifox**: 国产 API 管理工具
- **curl**: 命令行工具
### 8.2 认证方式
所有需要权限的接口都需要在请求头中携带认证信息:
```http
Authorization: Bearer {token}
```
### 8.3 时间格式
所有时间字段统一使用 ISO 8601 格式:
```
2025-01-26T14:15:00.000Z
```
### 8.4 分页参数
所有分页接口统一使用以下参数:
- `pageNum`: 页码,从 1 开始
- `pageSize`: 页大小,默认 10
---
## 联系方式
如有 API 相关问题,请联系开发团队或查看项目仓库的 Issues。
@@ -143,7 +143,7 @@ CREATE TABLE `hq_sat_record` (
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`survey_id` bigint(20) NOT NULL COMMENT '问卷ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '答题用户ID(匿名时为NULL)',
`user_type` char(1) NOT NULL COMMENT '用户类型(1学生 2教职工)',
`user_type` char(1) NOT NULL COMMENT '用户类型(1学生 2教职工 3超级管理员)',
`user_dept` varchar(100) DEFAULT NULL COMMENT '用户部门(匿名时为NULL)',
`submit_time` datetime NOT NULL COMMENT '提交时间',
`total_score` decimal(10,2) DEFAULT '0.00' COMMENT '总分',
@@ -180,6 +180,73 @@ CREATE TABLE `hq_sat_detail` (
KEY `idx_answer_score` (`answer_score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答题详情表';
-- ==========================================
-- 答题答案表(新增)
-- ==========================================
DROP TABLE IF EXISTS `hq_sat_answer`;
CREATE TABLE `hq_sat_answer` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL 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代表删除)',
`record_id` bigint(20) NOT NULL COMMENT '答题记录ID',
`question_id` bigint(20) NOT NULL COMMENT '题目ID',
`question_type` char(1) NOT NULL COMMENT '题目类型(1单选题 2多选题 3问答题)',
`option_id` bigint(20) DEFAULT NULL COMMENT '选项ID(单选题、多选题用,问答题为NULL)',
`option_value` varchar(50) DEFAULT NULL COMMENT '选项值(A、B、C、D等)',
`option_text` varchar(500) DEFAULT NULL COMMENT '选项文本内容',
`answer_value` text COMMENT '答案值(单选题存选项值,多选题存逗号分隔选项值,问答题存文本内容)',
`answer_score` int(11) DEFAULT '0' COMMENT '该答案得分',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_record_id` (`record_id`),
KEY `idx_question_id` (`question_id`),
KEY `idx_question_type` (`question_type`),
KEY `idx_option_id` (`option_id`),
KEY `idx_record_question` (`record_id`, `question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答题答案表';
-- ==========================================
-- 数据表索引和约束说明
-- ==========================================
/*
hq_sat_answer 表设计说明:
1. 表用途:
- 记录用户答题的具体答案内容
- 支持单选题、多选题、问答题三种题型
- 与 hq_sat_detail 表配合使用,detail表记录总体信息,answer表记录具体答案
2. 字段说明:
- question_type:题目类型,便于按类型统计分析
- option_id:选项ID,单选题和多选题需要关联到具体选项
- option_value:选项值(如A、B、C、D),便于统计
- option_text:选项文本,冗余存储便于查询
- answer_value:核心字段,存储实际答案:
* 单选题:存储选中的选项值,如 "A"
* 多选题:存储多个选项值,如 "A,C,D"
* 问答题:存储用户输入的文本内容
- answer_score:该答案的得分
3. 数据存储示例:
- 单选题:question_type='1', option_id=123, option_value='A', option_text='非常满意', answer_value='A', answer_score=5
- 多选题:每个选中选项一条记录,或合并为一条记录存储 "A,C,D"
- 问答题:question_type='3', option_id=NULL, answer_value='用户输入的文本内容', answer_score=0
4. 与其他表的关系:
- record_id 关联 hq_sat_record.id
- question_id 关联 hq_sat_question.id
- option_id 关联 hq_sat_option.id(问答题为NULL)
5. 查询优化:
- 建立复合索引 (record_id, question_id) 提高关联查询效率
- 按题目类型、选项等建立索引便于统计分析
*/
-- ==========================================================================
-- 初始化数据
+56 -3
View File
@@ -1,6 +1,13 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { SatSurveyVO, SatSurveyForm, SatSurveyQuery } from '@/api/hq/answerSurvey/types';
import {
SatSurveyVO,
SatSurveyForm,
SatSurveyQuery,
SubmitSurveyAnswerForm,
SatRecordVO,
SurveyStatisticsVO
} from '@/api/hq/answerSurvey/types';
/**
* 查询问卷列表
@@ -134,7 +141,7 @@ export const getUserSurveyAnswer = (surveyId: string | number): AxiosPromise<any
* 提交问卷答案
* @param data 答案数据
*/
export const submitSurveyAnswer = (data: any) => {
export const submitSurveyAnswer = (data: SubmitSurveyAnswerForm): AxiosPromise<SatRecordVO> => {
return request({
url: '/hq/satSurvey/answer/submit',
method: 'post',
@@ -146,10 +153,56 @@ export const submitSurveyAnswer = (data: any) => {
* 保存问卷答案草稿
* @param data 答案数据
*/
export const saveSurveyDraft = (data: any) => {
export const saveSurveyDraft = (data: SubmitSurveyAnswerForm): AxiosPromise<SatRecordVO> => {
return request({
url: '/hq/satSurvey/answer/draft',
method: 'post',
data: data
});
};
/**
* 获取问卷统计信息
* @param surveyId 问卷ID
*/
export const getSurveyStatistics = (surveyId: string | number): AxiosPromise<SurveyStatisticsVO> => {
return request({
url: '/hq/satSurvey/' + surveyId + '/statistics',
method: 'get'
});
};
/**
* 获取用户答题记录详情
* @param recordId 答题记录ID
*/
export const getAnswerRecord = (recordId: string | number): AxiosPromise<SatRecordVO> => {
return request({
url: '/hq/satSurvey/answer/record/' + recordId,
method: 'get'
});
};
/**
* 获取问卷的所有答题记录列表
* @param surveyId 问卷ID
* @param query 查询参数
*/
export const getSurveyAnswerRecords = (surveyId: string | number, query?: any): AxiosPromise<SatRecordVO[]> => {
return request({
url: '/hq/satSurvey/' + surveyId + '/records',
method: 'get',
params: query
});
};
/**
* 删除答题记录
* @param recordId 答题记录ID
*/
export const deleteAnswerRecord = (recordId: string | number) => {
return request({
url: '/hq/satSurvey/answer/record/' + recordId,
method: 'delete'
});
};
+274
View File
@@ -212,5 +212,279 @@ export interface SatSurveyQuery extends PageQuery {
params?: any;
}
// ==========================================
// 答题相关类型定义
// ==========================================
/**
* 答题答案VO
*/
export interface SatAnswerVO {
/**
* 主键ID
*/
id: string | number;
/**
* 答题记录ID
*/
recordId: string | number;
/**
* 题目ID
*/
questionId: string | number;
/**
* 题目类型(1单选题 2多选题 3问答题)
*/
questionType: string;
/**
* 选项ID(单选题、多选题用,问答题为NULL)
*/
optionId?: string | number;
/**
* 选项值(A、B、C、D等)
*/
optionValue?: string;
/**
* 选项文本内容
*/
optionText?: string;
/**
* 答案值(单选题存选项值,多选题存逗号分隔选项值,问答题存文本内容)
*/
answerValue: string;
/**
* 该答案得分
*/
answerScore: number;
/**
* 备注
*/
remark?: string;
}
/**
* 答题记录VO
*/
export interface SatRecordVO {
/**
* 主键ID
*/
id: string | number;
/**
* 问卷ID
*/
surveyId: string | number;
/**
* 答题人ID
*/
userId: string | number;
/**
* 答题人姓名
*/
userName?: string;
/**
* 答题开始时间
*/
startTime: string;
/**
* 答题结束时间
*/
endTime?: string;
/**
* 答题用时(秒)
*/
duration?: number;
/**
* 总得分
*/
totalScore: number;
/**
* 答题状态(0草稿 1已提交)
*/
status: string;
/**
* 是否匿名
*/
isAnonymous: number;
/**
* 备注
*/
remark?: string;
/**
* 答题详情列表
*/
answers?: SatAnswerVO[];
}
/**
* 提交答题的单个答案
*/
export interface SubmitAnswerItem {
/**
* 题目ID
*/
questionId: string | number;
/**
* 题目类型(1单选题 2多选题 3问答题)
*/
questionType: string;
/**
* 答案值
* - 单选题:选项值,如 "A"
* - 多选题:多个选项值,如 "A,C,D"
* - 问答题:用户输入的文本内容
*/
answerValue: string;
/**
* 选项详情(单选题和多选题需要)
*/
options?: Array<{
optionId: string | number;
optionValue: string;
optionText: string;
isSelected: boolean;
}>;
}
/**
* 提交答题请求数据
*/
export interface SubmitSurveyAnswerForm {
/**
* 问卷ID
*/
surveyId: string | number;
/**
* 答题开始时间
*/
startTime: string;
/**
* 答题结束时间
*/
endTime: string;
/**
* 是否匿名(1是 0否)
*/
isAnonymous: number;
/**
* 答案列表
*/
answers: SubmitAnswerItem[];
/**
* 备注
*/
remark?: string;
}
/**
* 答题统计信息
*/
export interface SurveyStatisticsVO {
/**
* 问卷ID
*/
surveyId: string | number;
/**
* 问卷标题
*/
surveyTitle: string;
/**
* 总参与人数
*/
totalParticipants: number;
/**
* 已完成人数
*/
completedCount: number;
/**
* 草稿数量
*/
draftCount: number;
/**
* 平均用时(秒)
*/
avgDuration: number;
/**
* 平均得分
*/
avgScore: number;
/**
* 题目统计详情
*/
questionStats?: QuestionStatItem[];
}
/**
* 题目统计项
*/
export interface QuestionStatItem {
/**
* 题目ID
*/
questionId: string | number;
/**
* 题目标题
*/
questionTitle: string;
/**
* 题目类型
*/
questionType: string;
/**
* 回答人数
*/
answerCount: number;
/**
* 选项统计(单选题、多选题)
*/
optionStats?: Array<{
optionId: string | number;
optionText: string;
selectCount: number;
selectRate: number;
}>;
/**
* 问答题统计
*/
textAnswers?: string[];
}
+13 -5
View File
@@ -127,7 +127,7 @@ const dynamicTenantEvent = async (tenantId: string) => {
if (companyName.value != null && companyName.value !== '') {
await dynamicTenant(tenantId);
dynamic.value = true;
await proxy?.$router.push('/');
await router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
}
@@ -136,7 +136,7 @@ const dynamicTenantEvent = async (tenantId: string) => {
const dynamicClearEvent = async () => {
await dynamicClear();
dynamic.value = false;
await proxy?.$router.push('/');
await router.push('/');
await proxy?.$tab.closeAllPage();
await proxy?.$tab.refreshPage();
};
@@ -288,10 +288,18 @@ watch(
.user-avatar {
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 8px;
width: 30px !important;
height: 30px !important;
min-width: 30px;
min-height: 30px;
max-width: 30px;
max-height: 30px;
border-radius: 6px;
margin-top: 10px;
object-fit: cover;
object-position: center;
display: block;
flex-shrink: 0;
}
i {
+11 -1
View File
@@ -153,6 +153,10 @@ service.interceptors.response.use(
} else if (code === HttpStatus.WARN) {
ElMessage({ message: msg, type: 'warning' });
return Promise.reject(new Error(msg));
} else if (code === 404) {
// 对404错误进行特殊处理,不显示错误提示
console.log('404错误被静默处理,不显示用户提示');
return Promise.reject(new Error('404'));
} else if (code !== HttpStatus.SUCCESS) {
ElNotification.error({ title: msg });
return Promise.reject('error');
@@ -167,7 +171,13 @@ service.interceptors.response.use(
} else if (message.includes('timeout')) {
message = '系统接口请求超时';
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常';
const statusCode = message.substr(message.length - 3);
// 对404错误进行特殊处理,不显示错误提示
if (statusCode === '404') {
console.log('404错误被静默处理,不显示用户提示');
return Promise.reject(error);
}
message = '系统接口' + statusCode + '异常';
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
return Promise.reject(error);
File diff suppressed because it is too large. Load diff