Files
hzdyjs/docs/开发文档/API/后勤满意度调查API.md
T
2026-09-22 19:22:18 +08:00

682 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后勤满意度调查API文档
## 文档信息
- **版本**:1.0.1
- **最后更新**:2026-09-20
- **作者**:Cursor AI
- **分类**:后勤管理模块
## 概述
本文档详细说明后勤满意度调查系统的API接口设计,包括问卷管理、题目管理、选项管理、答题记录等功能。系统采用前后端分离架构,接口采用RESTful API风格设计,所有接口均需通过Token进行身份验证。
### 问卷过期处理(2026-09-20)
- 原依赖 SnailJob 任务 `surveyEndTimeJob` / `surveyEndTimeJobExecutor` 每小时扫描过期问卷。
- 已改为 Spring `@Scheduled`:`HqSatSurveyExpireTask`(cron:`0 0 * * * ?`),将 `hq_sat_survey` 中 `end_time` 已过期且未结束的记录 `status` 更新为 `2`。
- 多实例通过 Redis 锁 `hq:sat:survey:expire:lock` 避免重复执行。
- 测试库已关闭对应 SnailJob 任务(`job_status=0`),避免与 `@Scheduled` 双跑。
## 通用响应格式
### 成功响应
```json
{
"code": 200,
"msg": "操作成功",
"data": {} // 响应数据,具体结构取决于不同接口
}
```
### 分页数据响应
```json
{
"total": 100, // 总记录数
"rows": [], // 当前页记录列表
"code": 200, // 状态码
"msg": "查询成功" // 提示信息
}
```
### 错误响应
```json
{
"code": 500, // 错误码
"msg": "操作失败", // 错误消息
"data": null // 通常为空
}
```
## 认证与授权
所有接口均需要在请求头中携带Token进行身份验证:
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
## 接口清单
| 接口路径 | 方法 | 描述 | 权限标识 |
|---------|-----|------|---------|
| `/hq/satSurvey/list` | GET | 查询问卷列表 | hq:satSurvey:list |
| `/hq/satSurvey/{id}` | GET | 获取问卷详情 | hq:satSurvey:query |
| `/hq/satSurvey` | POST | 新增问卷 | hq:satSurvey:add |
| `/hq/satSurvey` | PUT | 修改问卷 | hq:satSurvey:edit |
| `/hq/satSurvey/{ids}` | DELETE | 删除问卷 | hq:satSurvey:remove |
| `/hq/satSurvey/publish/{id}` | PUT | 发布问卷 | hq:satSurvey:publish |
| `/hq/satQuestion/list` | GET | 查询题目列表 | hq:satQuestion:list |
> 📌 **排序策略更新(2025-11-13)**:题目列表在按指定字段排序时,会额外追加 `createTime DESC` 作为最后一位兜底排序,确保返回结果顺序稳定。
| `/hq/satQuestion/{id}` | GET | 获取题目详情 | hq:satQuestion:query |
| `/hq/satQuestion` | POST | 新增题目 | hq:satQuestion:add |
| `/hq/satQuestion` | PUT | 修改题目 | hq:satQuestion:edit |
| `/hq/satQuestion/{ids}` | DELETE | 删除题目 | hq:satQuestion:remove |
| **`/hq/satQuestion/{questionId}/options`** | GET | 获取题目的选项列表 | hq:satQuestion:query |
| **`/hq/satOption/list`** | GET | 查询题目选项列表 | hq:satOption:list |
| **`/hq/satOption/{id}`** | GET | 获取题目选项详情 | hq:satOption:query |
| **`/hq/satOption`** | POST | 新增题目选项 | hq:satOption:add |
| **`/hq/satOption`** | PUT | 修改题目选项 | hq:satOption:edit |
| **`/hq/satOption/{ids}`** | DELETE | 删除题目选项 | hq:satOption:remove |
| `/hq/satRecord/list` | GET | 查询答题记录列表 | hq:satRecord:list |
| `/hq/satRecord/{id}` | GET | 获取答题记录详情 | hq:satRecord:query |
| `/hq/satSurvey/{id}/statistics` | GET | 获取问卷统计分析 | hq:satSurvey:result |
| `/hq/satSurvey/{id}/records` | GET | 获取问卷答题记录 | hq:satSurvey:result |
| `/hq/satSurvey/{id}/exportReport` | POST | 导出调查报告 | hq:satSurvey:export |
## API详情
### 题目选项管理API
#### 1. 查询题目选项列表
**请求方式**:GET
**请求路径**:`/hq/satOption/list`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| questionId | Long | 否 | 题目ID |
| pageNum | Integer | 否 | 页码,默认1 |
| pageSize | Integer | 否 | 每页条数,默认10 |
**请求示例**:
```
GET /hq/satOption/list?questionId=1937751489358733313&pageNum=1&pageSize=10
```
**响应示例**:
```json
{
"total": 5,
"rows": [
{
"id": 1937810382736855041,
"questionId": 1937751489358733313,
"optionContent": "非常满意",
"optionScore": 5,
"optionOrder": 1,
"isDefault": "N",
"tenantId": "000000",
"createBy": 1,
"createTime": "2025-06-25 17:50:18",
"updateBy": 1,
"updateTime": "2025-06-25 17:50:18",
"deleted": "0"
},
{
"id": 1937810382736855042,
"questionId": 1937751489358733313,
"optionContent": "满意",
"optionScore": 4,
"optionOrder": 2,
"isDefault": "N",
"tenantId": "000000",
"createBy": 1,
"createTime": "2025-06-25 17:50:18",
"updateBy": 1,
"updateTime": "2025-06-25 17:50:18",
"deleted": "0"
}
// ... 更多选项数据
],
"code": 200,
"msg": "查询成功"
}
```
#### 2. 获取题目的选项列表
**请求方式**:GET
**请求路径**:`/hq/satQuestion/{questionId}/options`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| questionId | Long | 是 | 题目ID |
**请求示例**:
```
GET /hq/satQuestion/1937751489358733313/options
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{
"id": 1937810382736855041,
"questionId": 1937751489358733313,
"optionContent": "非常满意",
"optionScore": 5,
"optionOrder": 1,
"isDefault": "N",
"tenantId": "000000",
"createBy": 1,
"createTime": "2025-06-25 17:50:18",
"updateBy": 1,
"updateTime": "2025-06-25 17:50:18",
"deleted": "0"
},
{
"id": 1937810382736855042,
"questionId": 1937751489358733313,
"optionContent": "满意",
"optionScore": 4,
"optionOrder": 2,
"isDefault": "N",
"tenantId": "000000",
"createBy": 1,
"createTime": "2025-06-25 17:50:18",
"updateBy": 1,
"updateTime": "2025-06-25 17:50:18",
"deleted": "0"
},
// ... 更多选项数据
]
}
```
**前端使用说明**:
- 此接口返回指定题目的全部选项,数据结构为数组,不分页
- **实现变更(2025-07-17):**
- 接口现在根据questionId参数过滤选项,只返回特定题目的选项数据
- 返回的选项数据已按照optionOrder字段升序排序,无需前端再次排序
- 适用于问题选择界面或答题场景,可一次性获取题目所有选项
- 对于单选题和多选题应显示选项,对于问答题则不需要显示
#### 3. 获取题目选项详情
**请求方式**:GET
**请求路径**:`/hq/satOption/{id}`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| id | Long | 是 | 选项ID |
**请求示例**:
```
GET /hq/satOption/1937810382736855041
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1937810382736855041,
"questionId": 1937751489358733313,
"optionContent": "非常满意",
"optionScore": 5,
"optionOrder": 1,
"isDefault": "N",
"tenantId": "000000",
"createBy": 1,
"createTime": "2025-06-25 17:50:18",
"updateBy": 1,
"updateTime": "2025-06-25 17:50:18",
"deleted": "0"
}
}
```
#### 4. 新增题目选项
**请求方式**:POST
**请求路径**:`/hq/satOption`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| questionId | Long | 是 | 题目ID |
| optionContent | String | 是 | 选项内容 |
| optionScore | Integer | 否 | 选项分值,默认0 |
| optionOrder | Integer | 否 | 选项排序,默认0 |
| isDefault | String | 否 | 是否默认选项(Y是 N否),默认N |
**请求示例**:
```json
{
"questionId": 1937751489358733313,
"optionContent": "一般般",
"optionScore": 3,
"optionOrder": 3,
"isDefault": "N"
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
#### 5. 修改题目选项
**请求方式**:PUT
**请求路径**:`/hq/satOption`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| id | Long | 是 | 选项ID |
| questionId | Long | 是 | 题目ID |
| optionContent | String | 是 | 选项内容 |
| optionScore | Integer | 否 | 选项分值 |
| optionOrder | Integer | 否 | 选项排序 |
| isDefault | String | 否 | 是否默认选项(Y是 N否) |
**请求示例**:
```json
{
"id": 1937810382736855041,
"questionId": 1937751489358733313,
"optionContent": "非常满意",
"optionScore": 5,
"optionOrder": 1,
"isDefault": "Y"
}
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
#### 6. 删除题目选项
**请求方式**:DELETE
**请求路径**:`/hq/satOption/{ids}`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| ids | String | 是 | 选项ID,多个用逗号分隔 |
**请求示例**:
```
DELETE /hq/satOption/1937810382736855041,1937810382736855042
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
## 最佳实践
### 题目选项批量管理
虽然当前API不直接支持选项批量操作,但前端可以通过组合使用现有API实现选项的批量管理,步骤如下:
1. **加载选项**:使用 `GET /hq/satOption/list?questionId={questionId}` 获取题目当前所有选项
2. **处理更新**:
- 对于有ID的选项,调用 `PUT /hq/satOption` 更新内容
- 对于新增的选项,调用 `POST /hq/satOption` 创建
- 对比前后选项ID差异,对不存在的选项调用 `DELETE /hq/satOption/{ids}` 删除
这种实现方式允许在一次操作中完成多个选项的增删改,但需注意以下几点:
- 操作非事务性:多个API调用之间没有事务保证,如遇网络问题可能导致数据部分更新
- 性能考虑:选项数量大时,多次API调用可能影响性能
- 字段映射:注意前后端字段命名差异(如 `optionContent` → `optionText`)
**示例代码**:
```javascript
// 1. 获取当前题目的所有已有选项
const res = await listSatOption({ questionId: questionId });
const existingOptions = res.rows || [];
// 2. 处理选项数据
const promises = [];
// 新增或修改选项
for (const option of options) {
if (!option.optionContent) continue; // 跳过空选项
const params = {
id: option.id,
questionId: questionId,
optionText: option.optionContent,
optionValue: option.optionScore || 0,
sortOrder: option.optionOrder || 1,
status: '0',
isDefault: option.isDefault || 'N'
};
if (option.id) {
// 已有选项,执行更新
promises.push(updateSatOption(params));
} else {
// 新选项,执行新增
promises.push(addSatOption(params));
}
}
// 删除不再存在的选项
const currentOptionIds = 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);
```
**建议改进**:后续可考虑在后端添加批量操作API,如 `POST /hq/satOption/batch` 以提供事务保证和性能优化。
## 数据结构说明
### 题目选项表(hq_sat_option)
| 字段名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| id | bigint(20) | 是 | 选项ID,主键(雪花ID) |
| question_id | bigint(20) | 是 | 题目ID,关联题目表 |
| option_content | varchar(255) | 是 | 选项内容 |
| option_score | int(11) | 否 | 选项分值,用于计算满意度评分 |
| option_order | int(11) | 否 | 选项排序,控制前端显示顺序 |
| is_default | char(1) | 否 | 是否默认选项(Y是 N否) |
| tenant_id | varchar(20) | 是 | 租户编号 |
| create_by | bigint(20) | 否 | 创建者 |
| create_time | datetime | 否 | 创建时间 |
| update_by | bigint(20) | 否 | 更新者 |
| update_time | datetime | 否 | 更新时间 |
| deleted | char(1) | 否 | 删除标志(0代表存在 1代表删除) |
## 错误码说明
| 错误码 | 描述 | 解决方案 |
|-------|-----|---------|
| 200 | 操作成功 | 请求正常处理完毕 |
| 401 | 认证失败 | 检查用户Token是否正确 |
| 403 | 权限不足 | 检查用户是否有操作权限 |
| 404 | 资源不存在 | 检查请求的资源是否存在 |
| 500 | 系统内部错误 | 联系系统管理员 |
## 变更记录
| 版本号 | 更新日期 | 更新内容 | 更新人 |
|-------|---------|---------|-------|
| 1.0.0 | 2025-07-01 | 初始版本:创建满意度调查API文档 | Cursor |
| 1.0.0 | 2025-07-01 | 更新题目选项表(hq_sat_option)结构,完善选项管理API | Cursor |
| 1.0.1 | 2025-07-17 | 优化题目选项获取接口,增强前端题目选项显示功能 | Cursor |
| 1.0.2 | 2025-07-17 | 修复选项排序值保存后实时更新功能,优化用户体验 | wuzilong |
| 1.0.3 | 2025-07-17 | 添加选项保存防重复提交机制,解决幂等性控制冲突 | wuzilong |
| 1.0.4 | 2025-07-18 | 优化问答题文本回答展示功能,提高Vue渲染稳定性 | wuzilong |
| 1.0.5 | 2025-07-18 | 增强问卷答题记录接口功能,返回答题详情数据 | wuzilong |
| 1.0.6 | 2025-11-13 | 调整题目列表排序策略,追加创建时间倒序兜底,避免排序不稳定 | Cursor |
## 附录
### 选项分值计算说明
选项分值(option_score)用于计算满意度评分,在统计分析时使用,具体规则如下:
1. 单选题:直接使用所选选项的分值作为该题得分
2. 多选题:将所选所有选项的分值相加作为该题得分
3. 问答题:不计分,分值为0
满意度问卷总分计算方式:
- 总分(total_score) = 所有题目的得分之和
- 平均分(avg_score) = 总分 ÷ 有分值的题目数量(不包括问答题)
### 问答题文本回答数据结构
**统计分析接口中问答题文本回答数据结构**:
问卷统计分析接口(`/hq/satSurvey/{id}/statistics`)返回的问答题文本回答数据结构如下:
```json
"textAnswers": [
{
"id": 393,
"answer_text": "用户回答内容示例",
"text_length": 12,
"answer_time": "2025-07-18 10:40:35"
}
]
```
**字段说明**:
| 字段名 | 类型 | 描述 |
|-------|-----|------|
| id | Integer | 回答ID,唯一标识 |
| answer_text | String | 回答文本内容 |
| text_length | Integer | 文本长度(字符数) |
| answer_time | String | 回答时间,格式:yyyy-MM-dd HH:mm:ss |
**前端使用注意事项**:
- 在Vue循环渲染textAnswers数组时,应使用answer.id作为key值,而非索引,确保Vue的虚拟DOM正确更新
- 示例:`<div v-for="(answer, idx) in question.textAnswers.slice(0, 5)" :key="answer.id || idx" class="answer-item">`
- 2025-07-18修复:解决了文本回答因key不唯一导致的渲染问题,详见`docs/变更管理/code-adjust-record-2025-07-18-wuzilong.md`
#### 2025-07-18 接口返回顺序与问答题答案优化
- `/hq/satSurvey/record/{recordId}` 接口返回参数调整:
1. `answers` 数组顺序严格按照问卷题目顺序返回。
2. `answers.options` 选项数组顺序严格按照题目原始选项顺序返回。
3. 问答题(`questionType=3`)的 `answerValue` 字段取自 `remark` 字段,真实还原用户填写内容。
> 以上调整保证了前端展示与问卷设计一致,提升了数据准确性。
### 获取问卷的所有答题记录列表
**请求方式**:GET
**请求路径**:`/hq/satSurvey/{surveyId}/records`
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|-------|-----|------|------|
| surveyId | Long | 是 | 问卷ID,路径参数 |
| status | String | 否 | 答题状态(1-已提交,0-未提交) |
| userName | String | 否 | 用户姓名,模糊查询 |
| dateRange | String[] | 否 | 日期范围 |
| pageNum | Integer | 否 | 页码,默认1 |
| pageSize | Integer | 否 | 每页条数,默认10 |
**请求示例**:
```
GET /hq/satSurvey/1945836064483778562/records?pageNum=1&pageSize=10&surveyId=1945836064483778562
```
**响应示例**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 5,
"rows": [
{
"user_id": 2008,
"userName": "智控学院郑十",
"deptName": "智能控制学院",
"id": "1946035501017587714",
"survey_id": "1945836064483778562",
"submit_time": "2025-07-18 10:33:14",
"total_score": "24.00",
"create_time": "2025-07-18 10:33:14",
"status": "1",
"duration": 313,
"startTime": "2025-07-18T10:33:14",
"endTime": "2025-07-18T10:33:14",
"answers": [
{
"id": 1945836064483778563,
"questionId": 1945836064483778563,
"questionTitle": "您对后勤服务总体满意度如何?",
"questionType": "1",
"score": 4.0,
"answerValue": "B",
"options": [
{
"optionId": 1945836064483778569,
"optionValue": "B",
"optionText": "满意",
"isSelected": true
}
]
},
{
"id": 1945836064483778564,
"questionId": 1945836064483778564,
"questionTitle": "您认为校园环境有哪些需要改善的地方?",
"questionType": "2",
"score": 11.0,
"answerValue": "A,C,D",
"options": [
{
"optionId": 1945836064483778570,
"optionValue": "A",
"optionText": "绿化维护",
"isSelected": true
},
{
"optionId": 1945836064483778572,
"optionValue": "C",
"optionText": "卫生状况",
"isSelected": true
},
{
"optionId": 1945836064483778573,
"optionValue": "D",
"optionText": "基础设施",
"isSelected": true
}
]
},
{
"id": 1945836064483778565,
"questionId": 1945836064483778565,
"questionTitle": "您对校园后勤服务的建议",
"questionType": "3",
"score": 0,
"answerValue": "希望增加自习室的开放时间,目前晚上关门太早"
}
]
},
{
"user_id": 4,
"userName": "仅本人 密码666666",
"deptName": "行政单位",
"id": null,
"survey_id": null,
"submit_time": null,
"total_score": 0,
"create_time": null,
"status": "0",
"duration": 0
}
]
}
}
```
**特殊说明**:
1. **功能强化(2025-07-18)**:接口现在返回已提交记录的答题详情(`answers`字段),无需额外请求
2. 每条记录(已提交状态)包含完整的答题详情数据,包括:
- 题目信息:`questionId`、`questionTitle`、`questionType`
- 答案内容:`score`、`answerValue`
- 选项数据(单选/多选题):`options`数组,包含所有选中的选项
3. 问题类型(`questionType`):
- `1`:单选题
- `2`:多选题
- `3`:问答题
4. 未提交的记录(`status="0"`)不包含答题详情
**最佳实践**:
- 前端可直接使用返回的`answers`数据渲染答题详情,无需额外请求
- 单选题和多选题显示选项,问答题显示文本内容
### 相关文件
- 数据库表结构:`docs/设计文档/database/hzdyjs_xg.sql`
- 变更记录:`docs/变更管理/code-adjust-record-2025-07-01-cursor.md`
- 文本回答展示修复:`docs/变更管理/code-adjust-record-2025-07-18-wuzilong.md`
- 答题详情功能优化:`docs/变更管理/code-adjust-record-2025-07-18-wuzilong.md`
## FAQ 常见问题解答
### 1. 问卷答题记录查询报错 "Unknown column 'u.deleted' in 'where clause'"
**问题描述**:
获取问卷答题记录列表时报错:`java.sql.SQLSyntaxErrorException: Unknown column 'u.deleted' in 'where clause'`
**原因分析**:
在SQL查询中使用了 `u.deleted = 0` 条件,但实际上 `sys_user` 表中逻辑删除字段名是 `del_flag` 而非 `deleted`。这是系统表与业务表之间的字段命名差异导致的。
**解决方案**:
修改SQL查询,将 `WHERE u.deleted = 0` 改为 `WHERE u.del_flag = '0'`(注意类型也从数字改为了字符串)。
**相关代码**:
```java
// 修改前
sql.append("WHERE u.deleted = 0 ");
// 修改后
sql.append("WHERE u.del_flag = '0' ");
```
**注意事项**:
- 系统表(如sys_user, sys_role等)使用 `del_flag` 作为逻辑删除字段,类型为char(1)
- 业务表(如hq_sat_survey等)使用 `deleted` 作为逻辑删除字段,类型为tinyint(1)
- 在跨表关联查询时,务必注意这一差异,避免类似错误