2025年11月12日
This commit is contained in:
parent
6473c94e1c
commit
6805ed2981
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.compile.nullAnalysis.mode": "disabled"
|
||||
}
|
||||
|
|
@ -51,3 +51,46 @@ export function regenerateQrcode(qrcodeId) {
|
|||
})
|
||||
}
|
||||
|
||||
// 扫描二维码(公开接口)
|
||||
export function scanQrcode(qrcodeCode) {
|
||||
return request({
|
||||
url: '/psychology/qrcode/scan/' + qrcodeCode,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成量表测评二维码
|
||||
export function generateScaleQrcode(scaleId) {
|
||||
return request({
|
||||
url: '/psychology/qrcode/generate/scale',
|
||||
method: 'post',
|
||||
params: { scaleId: scaleId }
|
||||
})
|
||||
}
|
||||
|
||||
// 生成报告查看二维码
|
||||
export function generateReportQrcode(reportId) {
|
||||
return request({
|
||||
url: '/psychology/qrcode/generate/report',
|
||||
method: 'post',
|
||||
params: { reportId: reportId }
|
||||
})
|
||||
}
|
||||
|
||||
// 生成测评查看报告二维码
|
||||
export function generateAssessmentQrcode(assessmentId) {
|
||||
return request({
|
||||
url: '/psychology/qrcode/generate/assessment',
|
||||
method: 'post',
|
||||
params: { assessmentId: assessmentId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取二维码图片(Base64)
|
||||
export function getQrcodeImage(qrcodeId) {
|
||||
return request({
|
||||
url: '/psychology/qrcode/image/' + qrcodeId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
69
ruoyi-ui/src/api/psychology/questionnaireAnswer.js
Normal file
69
ruoyi-ui/src/api/psychology/questionnaireAnswer.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 查询问卷答题列表
|
||||
export function listQuestionnaireAnswer(query) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询问卷答题详细
|
||||
export function getQuestionnaireAnswer(answerId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/' + answerId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 开始问卷答题
|
||||
export function startQuestionnaireAnswer(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/start',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取问卷题目列表
|
||||
export function getQuestionnaireItems(questionnaireId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/items/' + questionnaireId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取答题答案详情列表
|
||||
export function getAnswerDetails(answerId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/details/' + answerId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存答案
|
||||
export function saveQuestionnaireAnswer(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/save',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 提交问卷
|
||||
export function submitQuestionnaireAnswer(answerId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/submit/' + answerId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取问卷成绩排名列表
|
||||
export function getQuestionnaireRankList(questionnaireId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/answer/rank/' + questionnaireId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
44
ruoyi-ui/src/api/psychology/questionnaireItem.js
Normal file
44
ruoyi-ui/src/api/psychology/questionnaireItem.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 查询问卷题目列表
|
||||
export function listQuestionnaireItem(questionnaireId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/item/list/' + questionnaireId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询题目详细
|
||||
export function getQuestionnaireItem(itemId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/item/' + itemId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增题目
|
||||
export function addQuestionnaireItem(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/item',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改题目
|
||||
export function updateQuestionnaireItem(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/item',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除题目
|
||||
export function delQuestionnaireItem(itemIds) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/item/' + itemIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
44
ruoyi-ui/src/api/psychology/questionnaireOption.js
Normal file
44
ruoyi-ui/src/api/psychology/questionnaireOption.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 查询问卷选项列表
|
||||
export function listQuestionnaireOption(itemId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/option/list/' + itemId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询问卷选项详细
|
||||
export function getQuestionnaireOption(optionId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/option/' + optionId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增问卷选项
|
||||
export function addQuestionnaireOption(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/option',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改问卷选项
|
||||
export function updateQuestionnaireOption(data) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/option',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除问卷选项
|
||||
export function delQuestionnaireOption(optionId) {
|
||||
return request({
|
||||
url: '/psychology/questionnaire/option/' + optionId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -10,9 +10,10 @@ export function listReport(query) {
|
|||
}
|
||||
|
||||
// 查询报告详细
|
||||
export function getReport(reportId) {
|
||||
export function getReport(reportId, sourceType) {
|
||||
const url = '/psychology/report/' + reportId + (sourceType ? '?sourceType=' + sourceType : '');
|
||||
return request({
|
||||
url: '/psychology/report/' + reportId,
|
||||
url: url,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
|
@ -59,3 +60,18 @@ export function generateReport(assessmentId) {
|
|||
})
|
||||
}
|
||||
|
||||
// 导出报告(Excel格式)
|
||||
export function exportReport(reportIds, queryParams) {
|
||||
// 构建参数对象
|
||||
const params = { ...queryParams }
|
||||
if (reportIds && reportIds.length > 0) {
|
||||
params.reportIds = reportIds.join(',')
|
||||
}
|
||||
return request({
|
||||
url: '/psychology/report/export',
|
||||
method: 'post',
|
||||
params: params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,3 +94,15 @@ export function extractText(file) {
|
|||
})
|
||||
}
|
||||
|
||||
// 导出量表(JSON格式)
|
||||
export function exportScale(scaleIds) {
|
||||
// 使用download方法处理文件下载,参数通过URL传递
|
||||
const params = scaleIds && scaleIds.length > 0 ? { scaleIds: scaleIds.join(',') } : {}
|
||||
return request({
|
||||
url: '/psychology/scale/export',
|
||||
method: 'post',
|
||||
params: params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ export const constantRoutes = [
|
|||
component: () => import('@/views/register'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/psychology/qrcode/scan/:qrcodeCode',
|
||||
component: () => import('@/views/psychology/qrcode/scan'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
component: () => import('@/views/error/404'),
|
||||
|
|
@ -342,14 +347,47 @@ export const dynamicRoutes = [
|
|||
roles: ['admin']
|
||||
}
|
||||
},
|
||||
// 问卷管理
|
||||
// 自定义问卷
|
||||
{
|
||||
path: 'questionnaire',
|
||||
name: 'Questionnaire',
|
||||
component: () => import('@/views/psychology/questionnaire/index'),
|
||||
meta: {
|
||||
title: '问卷管理',
|
||||
icon: 'copy',
|
||||
title: '自定义问卷',
|
||||
icon: 'edit',
|
||||
roles: ['admin']
|
||||
}
|
||||
},
|
||||
// 问卷开始答题
|
||||
{
|
||||
path: 'questionnaire/start',
|
||||
name: 'QuestionnaireStart',
|
||||
component: () => import('@/views/psychology/questionnaire/start'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
title: '开始答题',
|
||||
roles: ['admin', 'common']
|
||||
}
|
||||
},
|
||||
// 问卷答题
|
||||
{
|
||||
path: 'questionnaire/taking',
|
||||
name: 'QuestionnaireTaking',
|
||||
component: () => import('@/views/psychology/questionnaire/taking'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
title: '问卷答题',
|
||||
roles: ['admin', 'common']
|
||||
}
|
||||
},
|
||||
// 问卷题目管理
|
||||
{
|
||||
path: 'questionnaire/item',
|
||||
name: 'QuestionnaireItem',
|
||||
component: () => import('@/views/psychology/questionnaire/item'),
|
||||
hidden: true,
|
||||
meta: {
|
||||
title: '问卷题目管理',
|
||||
roles: ['admin']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ service.interceptors.response.use(res => {
|
|||
const msg = errorCode[code] || res.data.msg || errorCode['default']
|
||||
// 二进制数据则直接返回
|
||||
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||
// 对于blob类型,直接返回blob数据
|
||||
// 错误处理在error拦截器中处理
|
||||
return res.data
|
||||
}
|
||||
if (code === 401) {
|
||||
|
|
@ -107,9 +109,23 @@ service.interceptors.response.use(res => {
|
|||
return res.data
|
||||
}
|
||||
},
|
||||
error => {
|
||||
async error => {
|
||||
console.log('err' + error)
|
||||
let { message } = error
|
||||
// 对于blob类型的错误响应,尝试解析错误信息
|
||||
if (error.response && error.response.config &&
|
||||
(error.response.config.responseType === 'blob' || error.response.config.responseType === 'arraybuffer') &&
|
||||
error.response.data instanceof Blob) {
|
||||
try {
|
||||
const text = await error.response.data.text()
|
||||
const errorObj = JSON.parse(text)
|
||||
message = errorObj.error || errorObj.msg || '导出失败'
|
||||
// 对于文件导出错误,不自动显示Message,让调用方处理
|
||||
return Promise.reject(new Error(message))
|
||||
} catch (e) {
|
||||
// 解析失败,使用默认错误信息
|
||||
}
|
||||
}
|
||||
if (message == "Network Error") {
|
||||
message = "后端接口连接异常"
|
||||
} else if (message.includes("timeout")) {
|
||||
|
|
@ -117,8 +133,13 @@ service.interceptors.response.use(res => {
|
|||
} else if (message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常"
|
||||
}
|
||||
Message({ message: message, type: 'error', duration: 5 * 1000 })
|
||||
return Promise.reject(error)
|
||||
// 对于文件导出错误,不自动显示Message,让调用方处理
|
||||
const isExportError = error.response && error.response.config &&
|
||||
(error.response.config.responseType === 'blob' || error.response.config.responseType === 'arraybuffer')
|
||||
if (!isExportError) {
|
||||
Message({ message: message, type: 'error', duration: 5 * 1000 })
|
||||
}
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,32 @@
|
|||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>选择量表开始测评</span>
|
||||
<span>选择量表或问卷开始测评</span>
|
||||
<el-button style="float: right;" type="text" @click="handleBack">返回</el-button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
||||
<el-form-item label="选择量表" prop="scaleId">
|
||||
<el-select v-model="form.scaleId" placeholder="请选择要测评的量表" style="width: 100%;" filterable>
|
||||
<el-form-item label="选择量表/问卷" prop="scaleId">
|
||||
<el-select v-model="form.scaleId" placeholder="请选择要测评的量表或问卷" style="width: 100%;" filterable>
|
||||
<el-option
|
||||
v-for="scale in scaleList"
|
||||
:key="scale.scaleId"
|
||||
:label="scale.scaleName"
|
||||
:value="scale.scaleId">
|
||||
<span style="float: left">{{ scale.scaleName }}</span>
|
||||
<span style="float: left">
|
||||
<el-tag v-if="scale.sourceType === 'questionnaire'" type="warning" size="mini" style="margin-right: 5px;">问卷</el-tag>
|
||||
<el-tag v-else type="primary" size="mini" style="margin-right: 5px;">量表</el-tag>
|
||||
{{ scale.scaleName }}
|
||||
</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ scale.itemCount }}题</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">选择被测评人</el-divider>
|
||||
<el-divider content-position="left">选择被测评人(仅量表需要)</el-divider>
|
||||
|
||||
<el-form-item label="选择用户档案" prop="profileId">
|
||||
<el-select v-model="form.profileId" placeholder="请从存档用户中选择" style="width: 100%; filterable">
|
||||
<el-form-item label="选择用户档案" prop="profileId" :rules="getProfileRules()">
|
||||
<el-select v-model="form.profileId" placeholder="请从存档用户中选择(问卷不需要)" style="width: 100%; filterable">
|
||||
<template v-if="profileList.length === 0">
|
||||
<el-option disabled :label="'暂无存档用户,请先添加'" :value="''" />
|
||||
</template>
|
||||
|
|
@ -38,10 +42,13 @@
|
|||
</el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||
<i class="el-icon-info"></i> 提示:选择问卷时不需要选择用户档案
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading" :disabled="profileList.length === 0">开始测评</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading">开始测评/答题</el-button>
|
||||
<el-button @click="handleBack">取消</el-button>
|
||||
<el-button type="info" @click="redirectToProfile">管理用户档案</el-button>
|
||||
</el-form-item>
|
||||
|
|
@ -94,7 +101,7 @@ export default {
|
|||
},
|
||||
rules: {
|
||||
scaleId: [
|
||||
{ required: true, message: "请选择量表", trigger: "change" }
|
||||
{ required: true, message: "请选择量表或问卷", trigger: "change" }
|
||||
],
|
||||
profileId: [
|
||||
{ required: true, message: "请从存档用户中选择", trigger: "change" }
|
||||
|
|
@ -103,6 +110,11 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
// 检查URL参数中是否有scaleId
|
||||
const scaleId = this.$route.query.scaleId;
|
||||
if (scaleId) {
|
||||
this.form.scaleId = parseInt(scaleId);
|
||||
}
|
||||
this.loadScales();
|
||||
this.loadProfiles();
|
||||
this.loadPaused();
|
||||
|
|
@ -117,23 +129,47 @@ export default {
|
|||
// 判断是否是管理员:userId === 1 或者 roles 中包含 'admin'
|
||||
const isAdmin = userId === 1 || (roles && roles.includes('admin'));
|
||||
|
||||
// 如果是管理员,显示所有量表;否则只显示有权限的量表
|
||||
// 如果是管理员,显示所有量表和问卷;否则只显示有权限的量表
|
||||
if (isAdmin) {
|
||||
// 管理员显示所有量表
|
||||
listScale({ status: '0' }).then(response => {
|
||||
// 管理员显示所有量表和问卷
|
||||
listScale({ status: '0', includeQuestionnaire: true }).then(response => {
|
||||
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
|
||||
// 如果URL中有scaleId,检查是否是问卷
|
||||
if (this.form.scaleId) {
|
||||
const selectedScale = this.scaleList.find(s => s.scaleId === this.form.scaleId);
|
||||
if (selectedScale && selectedScale.sourceType === 'questionnaire') {
|
||||
// 如果是问卷,直接跳转到问卷开始页面
|
||||
const questionnaireId = selectedScale.originalId || Math.abs(selectedScale.scaleId);
|
||||
this.$router.replace({
|
||||
path: '/psychology/questionnaire/start',
|
||||
query: { questionnaireId: questionnaireId }
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("加载量表列表失败:", error);
|
||||
this.$message.error('加载量表列表失败,请稍后重试');
|
||||
});
|
||||
} else {
|
||||
// 普通用户只显示有权限的量表
|
||||
// 普通用户只显示有权限的量表(问卷不需要权限,也可以显示)
|
||||
if (!userId || isNaN(userId) || userId <= 0) {
|
||||
console.error("用户ID无效:", userId);
|
||||
// 如果userId无效,默认显示所有量表(可能是临时会话问题)
|
||||
console.warn("用户ID无效,尝试加载所有量表");
|
||||
listScale({ status: '0' }).then(response => {
|
||||
// 如果userId无效,默认显示所有量表和问卷(可能是临时会话问题)
|
||||
console.warn("用户ID无效,尝试加载所有量表和问卷");
|
||||
listScale({ status: '0', includeQuestionnaire: true }).then(response => {
|
||||
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
|
||||
// 如果URL中有scaleId,检查是否是问卷
|
||||
if (this.form.scaleId) {
|
||||
const selectedScale = this.scaleList.find(s => s.scaleId === this.form.scaleId);
|
||||
if (selectedScale && selectedScale.sourceType === 'questionnaire') {
|
||||
// 如果是问卷,直接跳转到问卷开始页面
|
||||
const questionnaireId = selectedScale.originalId || Math.abs(selectedScale.scaleId);
|
||||
this.$router.replace({
|
||||
path: '/psychology/questionnaire/start',
|
||||
query: { questionnaireId: questionnaireId }
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("加载量表列表失败:", error);
|
||||
this.$message.error('加载量表列表失败,请稍后重试');
|
||||
|
|
@ -143,16 +179,30 @@ export default {
|
|||
import('@/api/psychology/permission').then(module => {
|
||||
module.getUserScaleIds(userId).then(permissionResponse => {
|
||||
const allowedScaleIds = permissionResponse.data || [];
|
||||
if (allowedScaleIds.length === 0) {
|
||||
this.scaleList = [];
|
||||
this.$message.warning('您还没有被分配任何量表权限,请联系管理员');
|
||||
return;
|
||||
}
|
||||
// 获取所有量表,然后过滤
|
||||
listScale({ status: '0' }).then(response => {
|
||||
this.scaleList = response.rows.filter(scale =>
|
||||
scale.itemCount > 0 && allowedScaleIds.includes(scale.scaleId)
|
||||
);
|
||||
// 获取所有量表和问卷,然后过滤
|
||||
listScale({ status: '0', includeQuestionnaire: true }).then(response => {
|
||||
this.scaleList = response.rows.filter(scale => {
|
||||
if (scale.itemCount === 0) return false;
|
||||
// 问卷不需要权限,直接显示
|
||||
if (scale.sourceType === 'questionnaire') return true;
|
||||
// 量表需要检查权限
|
||||
return allowedScaleIds.includes(scale.scaleId);
|
||||
});
|
||||
if (this.scaleList.length === 0) {
|
||||
this.$message.warning('您还没有被分配任何量表权限,请联系管理员');
|
||||
}
|
||||
// 如果URL中有scaleId,检查是否是问卷
|
||||
if (this.form.scaleId) {
|
||||
const selectedScale = this.scaleList.find(s => s.scaleId === this.form.scaleId);
|
||||
if (selectedScale && selectedScale.sourceType === 'questionnaire') {
|
||||
// 如果是问卷,直接跳转到问卷开始页面
|
||||
const questionnaireId = selectedScale.originalId || Math.abs(selectedScale.scaleId);
|
||||
this.$router.replace({
|
||||
path: '/psychology/questionnaire/start',
|
||||
query: { questionnaireId: questionnaireId }
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("加载量表列表失败:", error);
|
||||
this.$message.error('加载量表列表失败,请稍后重试');
|
||||
|
|
@ -191,43 +241,85 @@ export default {
|
|||
this.pausedList = response.data || [];
|
||||
});
|
||||
},
|
||||
/** 获取用户档案验证规则(动态) */
|
||||
getProfileRules() {
|
||||
// 检查选中的是否是问卷
|
||||
const selectedScale = this.scaleList.find(s => s.scaleId === this.form.scaleId);
|
||||
if (selectedScale && selectedScale.sourceType === 'questionnaire') {
|
||||
// 问卷不需要用户档案
|
||||
return [];
|
||||
}
|
||||
// 量表需要用户档案
|
||||
return [
|
||||
{ required: true, message: "请从存档用户中选择", trigger: "change" }
|
||||
];
|
||||
},
|
||||
/** 提交表单 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.loading = true;
|
||||
// 找到选中的用户档案
|
||||
const selectedProfile = this.profileList.find(p => p.profileId === this.form.profileId);
|
||||
if (selectedProfile) {
|
||||
// 构建测评表单数据
|
||||
const assessmentData = {
|
||||
scaleId: this.form.scaleId,
|
||||
// 移除profileId,后端AssessmentStartVO不需要这个字段
|
||||
assesseeName: selectedProfile.userName,
|
||||
// 从userName等信息中提取性别和年龄,或使用默认值
|
||||
assesseeGender: '0', // 默认性别
|
||||
assesseeAge: 0, // 默认年龄
|
||||
assesseePhone: selectedProfile.phone,
|
||||
anonymous: false // 添加anonymous字段,设置为非匿名
|
||||
};
|
||||
|
||||
startAssessment(assessmentData).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("测评已开始");
|
||||
const assessmentId = response.data;
|
||||
// 确保使用正确的路由路径
|
||||
this.$router.push({ path: '/psychology/assessment/taking', query: { assessmentId: assessmentId } });
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error('Failed to start assessment:', error);
|
||||
this.$modal.msgError("开始测评失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.$modal.msgError("请选择有效的用户档案");
|
||||
// 先检查是否选择了量表或问卷
|
||||
if (!this.form.scaleId) {
|
||||
this.$modal.msgError("请选择量表或问卷");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查选中的是量表还是问卷
|
||||
const selectedScale = this.scaleList.find(s => s.scaleId === this.form.scaleId);
|
||||
if (!selectedScale) {
|
||||
this.$modal.msgError("请选择有效的量表或问卷");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是问卷,直接跳转到问卷答题页面(不需要用户档案)
|
||||
if (selectedScale.sourceType === 'questionnaire') {
|
||||
const questionnaireId = selectedScale.originalId || Math.abs(selectedScale.scaleId);
|
||||
// 问卷不需要用户档案,直接跳转到问卷开始页面
|
||||
this.$router.push({
|
||||
path: '/psychology/questionnaire/start',
|
||||
query: { questionnaireId: questionnaireId }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是量表,需要验证用户档案
|
||||
this.$refs["form"].validateField('profileId', (error) => {
|
||||
if (error) {
|
||||
this.$modal.msgError("请选择用户档案");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证通过,创建测评记录
|
||||
this.loading = true;
|
||||
// 找到选中的用户档案
|
||||
const selectedProfile = this.profileList.find(p => p.profileId === this.form.profileId);
|
||||
if (selectedProfile) {
|
||||
// 构建测评表单数据
|
||||
const assessmentData = {
|
||||
scaleId: this.form.scaleId,
|
||||
// 移除profileId,后端AssessmentStartVO不需要这个字段
|
||||
assesseeName: selectedProfile.userName,
|
||||
// 从userName等信息中提取性别和年龄,或使用默认值
|
||||
assesseeGender: '0', // 默认性别
|
||||
assesseeAge: 0, // 默认年龄
|
||||
assesseePhone: selectedProfile.phone,
|
||||
anonymous: false // 添加anonymous字段,设置为非匿名
|
||||
};
|
||||
|
||||
startAssessment(assessmentData).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("测评已开始");
|
||||
const assessmentId = response.data;
|
||||
// 确保使用正确的路由路径
|
||||
this.$router.push({ path: '/psychology/assessment/taking', query: { assessmentId: assessmentId } });
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Failed to start assessment:', error);
|
||||
this.$modal.msgError("开始测评失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.$modal.msgError("请选择有效的用户档案");
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -91,9 +91,19 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="目标类型" align="center" prop="targetType" width="100" />
|
||||
<el-table-column label="目标ID" align="center" prop="targetId" width="100" />
|
||||
<el-table-column label="二维码" align="center" prop="qrcodeUrl" width="120">
|
||||
<el-table-column label="二维码" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<image-preview v-if="scope.row.qrcodeUrl" :src="scope.row.qrcodeUrl" :width="100" :height="100"/>
|
||||
<el-button
|
||||
v-if="!scope.row.qrcodeImageLoading && !scope.row.qrcodeUrl"
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleViewQrcode(scope.row)"
|
||||
>查看二维码</el-button>
|
||||
<div v-else-if="scope.row.qrcodeImageLoading" style="padding: 10px;">
|
||||
<i class="el-icon-loading"></i>
|
||||
</div>
|
||||
<image-preview v-else-if="scope.row.qrcodeUrl" :src="scope.row.qrcodeUrl" :width="80" :height="80"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="扫码次数" align="center" prop="scanCount" width="100" />
|
||||
|
|
@ -112,8 +122,14 @@
|
|||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleViewQrcode(scope.row)"
|
||||
>查看二维码</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
|
|
@ -148,91 +164,178 @@
|
|||
/>
|
||||
|
||||
<!-- 添加或修改二维码对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="二维码编码" prop="qrcodeCode">
|
||||
<el-input v-model="form.qrcodeCode" placeholder="留空则自动生成" :disabled="form.qrcodeId != undefined" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="二维码类型" prop="qrcodeType">
|
||||
<el-select v-model="form.qrcodeType" placeholder="请选择二维码类型" style="width: 100%">
|
||||
<el-option label="测评" value="test" />
|
||||
<el-option label="查看报告" value="view_report" />
|
||||
<el-option label="注册" value="register" />
|
||||
<el-option label="登录" value="login" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标类型" prop="targetType">
|
||||
<el-select v-model="form.targetType" placeholder="请选择目标类型" style="width: 100%">
|
||||
<el-option label="量表" value="scale" />
|
||||
<el-option label="测评" value="assessment" />
|
||||
<el-option label="报告" value="report" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目标ID" prop="targetId">
|
||||
<el-input-number v-model="form.targetId" :min="1" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="短链接" prop="shortUrl">
|
||||
<el-input v-model="form.shortUrl" placeholder="请输入短链接(可选)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="过期时间" prop="expireTime">
|
||||
<el-date-picker
|
||||
v-model="form.expireTime"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择过期时间(可选)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="0">有效</el-radio>
|
||||
<el-radio label="1">无效</el-radio>
|
||||
<el-radio label="2">已过期</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="二维码预览" v-if="form.qrcodeUrl">
|
||||
<image-preview :src="form.qrcodeUrl" :width="200" :height="200"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<!-- 二维码类型选择 -->
|
||||
<el-form-item label="二维码类型" prop="qrcodeType">
|
||||
<el-select
|
||||
v-model="form.qrcodeType"
|
||||
placeholder="请选择二维码类型"
|
||||
style="width: 100%"
|
||||
@change="handleQrcodeTypeChange"
|
||||
>
|
||||
<el-option label="测评(扫码开始测评)" value="test" />
|
||||
<el-option label="查看报告(扫码查看报告)" value="view_report" />
|
||||
<el-option label="注册(扫码注册)" value="register" />
|
||||
<el-option label="登录(扫码登录)" value="login" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 测评类型:选择量表 -->
|
||||
<el-form-item
|
||||
v-if="form.qrcodeType === 'test'"
|
||||
label="选择量表"
|
||||
prop="targetId"
|
||||
:rules="[{ required: true, message: '请选择要生成二维码的量表', trigger: 'change' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.targetId"
|
||||
placeholder="请选择要生成二维码的量表"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
clearable
|
||||
@change="handleScaleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="scale in scaleList"
|
||||
:key="scale.scaleId"
|
||||
:label="scale.scaleName + (scale.scaleCode ? ' (' + scale.scaleCode + ')' : '')"
|
||||
:value="scale.scaleId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 查看报告类型:选择报告 -->
|
||||
<el-form-item
|
||||
v-if="form.qrcodeType === 'view_report'"
|
||||
label="选择报告"
|
||||
prop="targetId"
|
||||
:rules="[{ required: true, message: '请选择要生成二维码的报告', trigger: 'change' }]"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.targetId"
|
||||
placeholder="请选择要生成二维码的报告"
|
||||
style="width: 100%"
|
||||
filterable
|
||||
clearable
|
||||
@change="handleReportChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="report in reportList"
|
||||
:key="report.reportId"
|
||||
:label="(report.reportTitle || '未命名报告') + ' (ID: ' + report.reportId + ')'"
|
||||
:value="report.reportId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 高级选项(可折叠) -->
|
||||
<el-form-item>
|
||||
<el-collapse v-model="advancedOptionsActive" style="border: none;">
|
||||
<el-collapse-item name="advanced" style="border: none;">
|
||||
<template slot="title">
|
||||
<span style="color: #909399; font-size: 14px;">
|
||||
<i class="el-icon-setting"></i> 高级选项(可选)
|
||||
</span>
|
||||
</template>
|
||||
<!-- 过期时间 -->
|
||||
<el-form-item label="过期时间" prop="expireTime" style="margin-bottom: 18px;">
|
||||
<el-date-picker
|
||||
v-model="form.expireTime"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="选择过期时间(留空则永久有效)"
|
||||
style="width: 100%"
|
||||
:picker-options="{
|
||||
disabledDate(time) {
|
||||
return time.getTime() < Date.now() - 8.64e7; // 不能选择今天之前的日期
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态" prop="status" style="margin-bottom: 18px;">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="0">有效</el-radio>
|
||||
<el-radio label="1">无效</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- 二维码编码(新增时可选输入,编辑时只读显示) -->
|
||||
<el-form-item
|
||||
label="二维码编码"
|
||||
prop="qrcodeCode"
|
||||
style="margin-bottom: 18px;"
|
||||
>
|
||||
<el-input
|
||||
v-if="form.qrcodeId != undefined"
|
||||
v-model="form.qrcodeCode"
|
||||
placeholder="二维码编码"
|
||||
disabled
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="form.qrcodeCode"
|
||||
placeholder="留空则自动生成(可输入已删除的编码进行重用)"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
<div v-if="form.qrcodeId == undefined" style="margin-top: 5px; font-size: 12px; color: #909399;">
|
||||
提示:留空将自动生成唯一编码。如果输入已删除的二维码编码,可以重新使用该编码。
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 短链接 -->
|
||||
<el-form-item label="短链接" prop="shortUrl" style="margin-bottom: 18px;">
|
||||
<el-input v-model="form.shortUrl" placeholder="请输入短链接(可选,留空则使用系统默认链接)" />
|
||||
</el-form-item>
|
||||
<!-- 备注 -->
|
||||
<el-form-item label="备注" prop="remark" style="margin-bottom: 0;">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注(可选)" />
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 二维码预览(仅编辑时显示) -->
|
||||
<el-form-item v-if="form.qrcodeId && form.qrcodeUrl" label="二维码预览">
|
||||
<div style="text-align: center;">
|
||||
<image-preview :src="form.qrcodeUrl" :width="200" :height="200"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 二维码预览对话框 -->
|
||||
<el-dialog title="二维码预览" :visible.sync="qrcodePreviewOpen" width="500px" append-to-body>
|
||||
<div v-if="qrcodePreviewData" style="text-align: center;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<img
|
||||
:src="qrcodePreviewData.qrcodeUrl"
|
||||
alt="二维码"
|
||||
style="max-width: 300px; max-height: 300px; border: 1px solid #dcdfe6; padding: 10px; background: #fff;"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="color: #606266; font-size: 14px;">二维码编码: {{ qrcodePreviewData.qrcodeCode }}</p>
|
||||
<p style="color: #909399; font-size: 12px; margin-top: 10px;">扫码次数: {{ qrcodePreviewData.scanCount || 0 }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="downloadQrcodePreview">下载二维码</el-button>
|
||||
<el-button @click="qrcodePreviewOpen = false">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listQrcode, getQrcode, delQrcode, addQrcode, updateQrcode, regenerateQrcode } from "@/api/psychology/qrcode";
|
||||
import { listQrcode, getQrcode, delQrcode, addQrcode, updateQrcode, regenerateQrcode, getQrcodeImage } from "@/api/psychology/qrcode";
|
||||
import { listScale } from "@/api/psychology/scale";
|
||||
import { listReport } from "@/api/psychology/report";
|
||||
|
||||
export default {
|
||||
name: "PsyQrcode",
|
||||
|
|
@ -257,6 +360,17 @@ export default {
|
|||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 二维码预览对话框
|
||||
qrcodePreviewOpen: false,
|
||||
qrcodePreviewData: null,
|
||||
// 量表列表(用于下拉选择)
|
||||
scaleList: [],
|
||||
// 报告列表(用于下拉选择)
|
||||
reportList: [],
|
||||
// 高级选项是否展开
|
||||
advancedOptionsActive: [],
|
||||
// 提交按钮加载状态
|
||||
submitLoading: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
|
|
@ -277,17 +391,103 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.loadScales();
|
||||
this.loadReports();
|
||||
},
|
||||
watch: {
|
||||
'form.qrcodeType'(newVal) {
|
||||
// 当二维码类型改变时,清空目标ID和目标类型,并自动设置目标类型
|
||||
if (newVal === 'test') {
|
||||
this.form.targetType = 'scale';
|
||||
this.form.targetId = undefined;
|
||||
} else if (newVal === 'view_report') {
|
||||
this.form.targetType = 'report';
|
||||
this.form.targetId = undefined;
|
||||
} else {
|
||||
// 注册和登录类型不需要目标
|
||||
this.form.targetType = undefined;
|
||||
this.form.targetId = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询二维码列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
listQrcode(this.queryParams).then(response => {
|
||||
this.qrcodeList = response.rows;
|
||||
this.qrcodeList = response.rows || [];
|
||||
// 为每个二维码添加图片加载状态
|
||||
this.qrcodeList.forEach(item => {
|
||||
this.$set(item, 'qrcodeImageLoading', false);
|
||||
this.$set(item, 'qrcodeUrl', null);
|
||||
});
|
||||
this.total = response.total;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 加载量表列表 */
|
||||
loadScales() {
|
||||
listScale({ status: '0', pageNum: 1, pageSize: 1000 }).then(response => {
|
||||
this.scaleList = response.rows || [];
|
||||
}).catch(() => {
|
||||
this.scaleList = [];
|
||||
});
|
||||
},
|
||||
/** 加载报告列表 */
|
||||
loadReports() {
|
||||
listReport({ pageNum: 1, pageSize: 1000 }).then(response => {
|
||||
this.reportList = response.rows || [];
|
||||
}).catch(() => {
|
||||
this.reportList = [];
|
||||
});
|
||||
},
|
||||
/** 查看二维码 */
|
||||
handleViewQrcode(row) {
|
||||
// 如果已经加载过,直接显示
|
||||
if (row.qrcodeUrl) {
|
||||
this.qrcodePreviewData = {
|
||||
qrcodeUrl: row.qrcodeUrl,
|
||||
qrcodeCode: row.qrcodeCode,
|
||||
scanCount: row.scanCount
|
||||
};
|
||||
this.qrcodePreviewOpen = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 否则加载二维码图片
|
||||
this.$set(row, 'qrcodeImageLoading', true);
|
||||
getQrcodeImage(row.qrcodeId).then(response => {
|
||||
if (response.code === 200 && response.data && response.data.qrcodeUrl) {
|
||||
row.qrcodeUrl = response.data.qrcodeUrl;
|
||||
this.qrcodePreviewData = {
|
||||
qrcodeUrl: response.data.qrcodeUrl,
|
||||
qrcodeCode: row.qrcodeCode,
|
||||
scanCount: row.scanCount
|
||||
};
|
||||
this.qrcodePreviewOpen = true;
|
||||
} else {
|
||||
this.$modal.msgError("获取二维码图片失败");
|
||||
}
|
||||
this.$set(row, 'qrcodeImageLoading', false);
|
||||
}).catch(error => {
|
||||
this.$set(row, 'qrcodeImageLoading', false);
|
||||
this.$modal.msgError(error.msg || "获取二维码图片失败");
|
||||
});
|
||||
},
|
||||
/** 量表选择改变 */
|
||||
handleScaleChange(value) {
|
||||
if (value) {
|
||||
this.form.targetType = 'scale';
|
||||
this.form.targetId = value;
|
||||
}
|
||||
},
|
||||
/** 报告选择改变 */
|
||||
handleReportChange(value) {
|
||||
if (value) {
|
||||
this.form.targetType = 'report';
|
||||
this.form.targetId = value;
|
||||
}
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
|
|
@ -308,8 +508,26 @@ export default {
|
|||
status: "0",
|
||||
remark: undefined
|
||||
};
|
||||
this.advancedOptionsActive = [];
|
||||
this.submitLoading = false;
|
||||
this.resetForm("form");
|
||||
},
|
||||
/** 二维码类型改变 */
|
||||
handleQrcodeTypeChange(value) {
|
||||
// 自动设置目标类型
|
||||
if (value === 'test') {
|
||||
this.form.targetType = 'scale';
|
||||
} else if (value === 'view_report') {
|
||||
this.form.targetType = 'report';
|
||||
}
|
||||
// 清空目标ID
|
||||
this.form.targetId = undefined;
|
||||
},
|
||||
/** 图片加载错误处理 */
|
||||
handleImageError(event) {
|
||||
console.error('二维码图片加载失败:', event);
|
||||
this.$modal.msgError("二维码图片加载失败");
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1;
|
||||
|
|
@ -346,17 +564,67 @@ export default {
|
|||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
// 验证必填字段
|
||||
if (this.form.qrcodeType === 'test' && !this.form.targetId) {
|
||||
this.$modal.msgError("请选择要生成二维码的量表");
|
||||
return;
|
||||
}
|
||||
if (this.form.qrcodeType === 'view_report' && !this.form.targetId) {
|
||||
this.$modal.msgError("请选择要生成二维码的报告");
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitLoading = true;
|
||||
|
||||
if (this.form.qrcodeId != undefined) {
|
||||
// 修改
|
||||
updateQrcode(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
this.submitLoading = false;
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "修改失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.submitLoading = false;
|
||||
this.$modal.msgError(error.msg || "修改失败");
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
addQrcode(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
this.submitLoading = false;
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("生成成功!二维码已创建");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
// 如果是新增,可以选择立即查看二维码
|
||||
if (response.data && response.data.qrcodeId) {
|
||||
this.$modal.confirm('二维码生成成功,是否立即查看?', '提示', {
|
||||
confirmButtonText: '查看',
|
||||
cancelButtonText: '稍后',
|
||||
type: 'success'
|
||||
}).then(() => {
|
||||
// 获取生成的二维码并显示
|
||||
getQrcodeImage(response.data.qrcodeId).then(imgResponse => {
|
||||
if (imgResponse.code === 200 && imgResponse.data && imgResponse.data.qrcodeUrl) {
|
||||
this.qrcodePreviewData = {
|
||||
qrcodeUrl: imgResponse.data.qrcodeUrl,
|
||||
qrcodeCode: response.data.qrcodeCode,
|
||||
scanCount: 0
|
||||
};
|
||||
this.qrcodePreviewOpen = true;
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "生成失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.submitLoading = false;
|
||||
this.$modal.msgError(error.msg || "生成失败");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -384,12 +652,54 @@ export default {
|
|||
this.$modal.confirm('是否确认重新生成二维码编号为"' + qrcodeId + '"?').then(() => {
|
||||
return regenerateQrcode(qrcodeId);
|
||||
}).then(response => {
|
||||
if (response.data && response.data.qrcodeBase64) {
|
||||
if (response.code === 200 && response.data && response.data.qrcodeUrl) {
|
||||
this.$modal.msgSuccess("重新生成成功");
|
||||
// 更新列表中的二维码
|
||||
row.qrcodeUrl = "data:image/png;base64," + response.data.qrcodeBase64;
|
||||
row.qrcodeUrl = response.data.qrcodeUrl;
|
||||
// 如果正在预览,也更新预览数据
|
||||
if (this.qrcodePreviewOpen && this.qrcodePreviewData && this.qrcodePreviewData.qrcodeCode === row.qrcodeCode) {
|
||||
this.qrcodePreviewData.qrcodeUrl = response.data.qrcodeUrl;
|
||||
}
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "重新生成失败");
|
||||
}
|
||||
}).catch(() => {});
|
||||
}).catch(error => {
|
||||
this.$modal.msgError(error.msg || "重新生成失败");
|
||||
});
|
||||
},
|
||||
/** 下载二维码预览 */
|
||||
downloadQrcodePreview() {
|
||||
if (!this.qrcodePreviewData || !this.qrcodePreviewData.qrcodeUrl) {
|
||||
this.$modal.msgWarning("二维码图片不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是Base64格式,直接下载
|
||||
if (this.qrcodePreviewData.qrcodeUrl.startsWith('data:image')) {
|
||||
const link = document.createElement('a');
|
||||
link.href = this.qrcodePreviewData.qrcodeUrl;
|
||||
link.download = `二维码_${this.qrcodePreviewData.qrcodeCode}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
// 如果是URL格式,使用fetch下载
|
||||
fetch(this.qrcodePreviewData.qrcodeUrl)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `二维码_${this.qrcodePreviewData.qrcodeCode}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => {
|
||||
this.$modal.msgError("下载失败:" + (error.message || "未知错误"));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
146
ruoyi-ui/src/views/psychology/qrcode/scan.vue
Normal file
146
ruoyi-ui/src/views/psychology/qrcode/scan.vue
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-card v-loading="loading" shadow="never">
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
<el-alert
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div v-else-if="qrcodeInfo" class="scan-success">
|
||||
<div class="info-header">
|
||||
<i class="el-icon-success" style="color: #67C23A; font-size: 48px;"></i>
|
||||
<h2>二维码扫描成功</h2>
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<p>正在为您跳转...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading-container">
|
||||
<i class="el-icon-loading" style="font-size: 48px; color: #409EFF;"></i>
|
||||
<p>正在处理二维码...</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { scanQrcode } from "@/api/psychology/qrcode";
|
||||
|
||||
export default {
|
||||
name: "QrcodeScan",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
qrcodeInfo: null,
|
||||
errorMessage: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.handleScan();
|
||||
},
|
||||
methods: {
|
||||
/** 处理二维码扫描 */
|
||||
handleScan() {
|
||||
const qrcodeCode = this.$route.params.qrcodeCode;
|
||||
if (!qrcodeCode) {
|
||||
this.errorMessage = "二维码编码不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
scanQrcode(qrcodeCode)
|
||||
.then(response => {
|
||||
this.loading = false;
|
||||
if (response && response.code === 200) {
|
||||
// AjaxResult的结构是 {code: 200, msg: "...", data: {...}}
|
||||
const data = response.data || {};
|
||||
this.qrcodeInfo = data.qrcode || data;
|
||||
const redirectUrl = data.redirectUrl;
|
||||
|
||||
if (redirectUrl) {
|
||||
// 立即跳转,不延迟,提升用户体验
|
||||
// 如果redirectUrl是绝对路径(以/开头),直接使用;否则使用完整路径
|
||||
if (redirectUrl.startsWith('/')) {
|
||||
this.$router.push(redirectUrl).catch(err => {
|
||||
console.error('路由跳转失败:', err);
|
||||
// 如果路由跳转失败,尝试使用window.location
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
} else {
|
||||
this.errorMessage = "无法获取跳转地址";
|
||||
}
|
||||
} else {
|
||||
this.errorMessage = (response && response.msg) || "扫描二维码失败";
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
console.error('扫描二维码失败:', error);
|
||||
// 尝试从error.response中获取错误信息
|
||||
if (error.response && error.response.data) {
|
||||
const errorData = error.response.data;
|
||||
// 如果error.response.data是对象,尝试解析
|
||||
if (typeof errorData === 'object') {
|
||||
this.errorMessage = errorData.msg || errorData.error || errorData.message || "扫描二维码失败,请重试";
|
||||
} else {
|
||||
this.errorMessage = "扫描二维码失败,请重试";
|
||||
}
|
||||
} else if (error.msg) {
|
||||
this.errorMessage = error.msg;
|
||||
} else if (error.message) {
|
||||
this.errorMessage = error.message;
|
||||
} else {
|
||||
this.errorMessage = "扫描二维码失败,请重试";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scan-success,
|
||||
.loading-container,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-header h2 {
|
||||
margin-top: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
margin-top: 20px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
margin-top: 20px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -111,8 +111,15 @@
|
|||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-document"
|
||||
@click="handleManageItems(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:query']"
|
||||
>题目管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
|
|
@ -368,6 +375,15 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
/** 题目管理 */
|
||||
handleManageItems(row) {
|
||||
const questionnaireId = row.questionnaireId
|
||||
const questionnaireName = row.questionnaireName
|
||||
this.$router.push({
|
||||
path: '/psychology/questionnaire/item',
|
||||
query: { questionnaireId: questionnaireId, questionnaireName: questionnaireName }
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const questionnaireIds = row.questionnaireId ? [row.questionnaireId] : this.ids
|
||||
|
|
|
|||
537
ruoyi-ui/src/views/psychology/questionnaire/item.vue
Normal file
537
ruoyi-ui/src/views/psychology/questionnaire/item.vue
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 题目管理界面 -->
|
||||
<div>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="21.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['psychology:questionnaire:add']"
|
||||
>新增题目</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['psychology:questionnaire:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="el-icon-back"
|
||||
size="mini"
|
||||
@click="handleBack"
|
||||
>返回</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="itemList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="itemNumber" width="80" />
|
||||
<el-table-column
|
||||
label="题目内容"
|
||||
align="left"
|
||||
prop="itemContent"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column label="题型" align="center" prop="itemType" width="100">
|
||||
<template slot-scope="scope">
|
||||
{{ getItemTypeName(scope.row.itemType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="必填" align="center" prop="isRequired" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.isRequired === '1' ? '是' : '否' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分值" align="center" prop="score" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.score || 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="250">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-setting"
|
||||
@click="handleManageOptions(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:query']"
|
||||
v-if="needsOptions(scope.row.itemType)"
|
||||
>选项</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改题目对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="题目序号" prop="itemNumber">
|
||||
<el-input-number v-model="form.itemNumber" :min="1" :max="1000" placeholder="请输入题目序号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题目类型" prop="itemType">
|
||||
<el-select v-model="form.itemType" placeholder="请选择题目类型" @change="handleItemTypeChange">
|
||||
<el-option label="单选题" value="radio" />
|
||||
<el-option label="多选题" value="checkbox" />
|
||||
<el-option label="判断题" value="boolean" />
|
||||
<el-option label="填空题" value="input" />
|
||||
<el-option label="排序题" value="sort" />
|
||||
<el-option label="计算题" value="calculate" />
|
||||
<el-option label="简答题" value="text" />
|
||||
<el-option label="问答题" value="textarea" />
|
||||
<el-option label="作文题" value="essay" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="题目内容" prop="itemContent">
|
||||
<el-input v-model="form.itemContent" type="textarea" :rows="4" placeholder="请输入题目内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否必填">
|
||||
<el-radio-group v-model="form.isRequired">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="题目分值">
|
||||
<el-input-number v-model="form.score" :min="0" :precision="2" :step="0.5" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选项管理对话框 -->
|
||||
<el-dialog title="选项管理" :visible.sync="optionOpen" width="900px" append-to-body>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAddOption"
|
||||
>新增选项</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="optionList" border>
|
||||
<el-table-column label="选项编码" align="center" prop="optionCode" width="100" />
|
||||
<el-table-column label="选项内容" align="left" prop="optionContent" />
|
||||
<el-table-column label="分值" align="center" prop="optionScore" width="100" />
|
||||
<el-table-column label="正确答案" align="center" prop="isCorrect" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.isCorrect === '1'" type="success">是</el-tag>
|
||||
<el-tag v-else type="info">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdateOption(scope.row, scope.$index)"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDeleteOption(scope.$index)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitOptions">保 存</el-button>
|
||||
<el-button @click="optionOpen = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选项编辑对话框 -->
|
||||
<el-dialog title="编辑选项" :visible.sync="optionEditOpen" width="600px" append-to-body>
|
||||
<el-form ref="optionForm" :model="optionForm" label-width="100px">
|
||||
<el-form-item label="选项编码">
|
||||
<el-input v-model="optionForm.optionCode" placeholder="如:A、B、C或1、2、3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选项内容" required>
|
||||
<el-input v-model="optionForm.optionContent" type="textarea" :rows="3" placeholder="请输入选项内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选项分值">
|
||||
<el-input-number v-model="optionForm.optionScore" :min="0" :precision="2" :step="0.5" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否正确答案">
|
||||
<el-radio-group v-model="optionForm.isCorrect">
|
||||
<el-radio label="1">是</el-radio>
|
||||
<el-radio label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="optionForm.sortOrder" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="saveOption">确 定</el-button>
|
||||
<el-button @click="optionEditOpen = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listQuestionnaireItem, getQuestionnaireItem, delQuestionnaireItem, addQuestionnaireItem, updateQuestionnaireItem } from "@/api/psychology/questionnaireItem";
|
||||
import { listQuestionnaireOption, addQuestionnaireOption, updateQuestionnaireOption, delQuestionnaireOption } from "@/api/psychology/questionnaireOption";
|
||||
|
||||
export default {
|
||||
name: "QuestionnaireItem",
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: true,
|
||||
// 选中数组
|
||||
ids: [],
|
||||
// 非单个禁用
|
||||
single: true,
|
||||
// 非多个禁用
|
||||
multiple: true,
|
||||
// 题目表格数据
|
||||
itemList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 选项弹出层
|
||||
optionOpen: false,
|
||||
// 选项编辑弹出层
|
||||
optionEditOpen: false,
|
||||
// 选项列表
|
||||
optionList: [],
|
||||
// 当前编辑的题目ID
|
||||
currentItemId: null,
|
||||
// 当前编辑的题目行
|
||||
currentItemRow: null,
|
||||
// 当前编辑的选项索引
|
||||
currentOptionIndex: -1,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
questionnaireId: undefined
|
||||
},
|
||||
// 问卷名称
|
||||
currentQuestionnaireName: '',
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 选项表单参数
|
||||
optionForm: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
itemNumber: [
|
||||
{ required: true, message: "题目序号不能为空", trigger: "blur" }
|
||||
],
|
||||
itemType: [
|
||||
{ required: true, message: "题目类型不能为空", trigger: "change" }
|
||||
],
|
||||
itemContent: [
|
||||
{ required: true, message: "题目内容不能为空", trigger: "blur" }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
const questionnaireId = this.$route.query.questionnaireId;
|
||||
const questionnaireName = this.$route.query.questionnaireName;
|
||||
if (questionnaireId) {
|
||||
this.queryParams.questionnaireId = questionnaireId;
|
||||
this.currentQuestionnaireName = questionnaireName || '';
|
||||
this.getList();
|
||||
} else {
|
||||
this.$modal.msgError("问卷ID不能为空");
|
||||
this.$router.push('/psychology/questionnaire');
|
||||
}
|
||||
// 更新页面标题
|
||||
if (questionnaireName) {
|
||||
document.title = questionnaireName + " - 题目管理";
|
||||
} else {
|
||||
document.title = "题目管理";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询题目列表 */
|
||||
getList() {
|
||||
if (!this.queryParams.questionnaireId) {
|
||||
this.$modal.msgError("问卷ID不能为空");
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
listQuestionnaireItem(this.queryParams.questionnaireId).then(response => {
|
||||
this.itemList = response.data || [];
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error("查询题目列表失败:", error);
|
||||
this.$modal.msgError("查询题目列表失败:" + (error.message || "未知错误"));
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 获取题型名称 */
|
||||
getItemTypeName(itemType) {
|
||||
const typeMap = {
|
||||
'radio': '单选题',
|
||||
'checkbox': '多选题',
|
||||
'boolean': '判断题',
|
||||
'input': '填空题',
|
||||
'sort': '排序题',
|
||||
'calculate': '计算题',
|
||||
'text': '简答题',
|
||||
'textarea': '问答题',
|
||||
'essay': '作文题'
|
||||
};
|
||||
return typeMap[itemType] || itemType;
|
||||
},
|
||||
/** 判断题型是否需要选项 */
|
||||
needsOptions(itemType) {
|
||||
return ['radio', 'checkbox', 'boolean', 'sort'].includes(itemType);
|
||||
},
|
||||
/** 题目类型改变 */
|
||||
handleItemTypeChange() {
|
||||
// 可以在这里添加一些逻辑
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false;
|
||||
this.reset();
|
||||
},
|
||||
// 表单重置
|
||||
reset() {
|
||||
this.form = {
|
||||
itemId: undefined,
|
||||
questionnaireId: this.queryParams.questionnaireId,
|
||||
itemNumber: undefined,
|
||||
itemContent: undefined,
|
||||
itemType: "radio",
|
||||
isRequired: "1",
|
||||
score: 0,
|
||||
sortOrder: 0,
|
||||
remark: undefined
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.itemId);
|
||||
this.single = selection.length != 1;
|
||||
this.multiple = !selection.length;
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset();
|
||||
this.open = true;
|
||||
this.title = "添加题目";
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset();
|
||||
const itemId = row.itemId;
|
||||
getQuestionnaireItem(itemId).then(response => {
|
||||
this.form = response.data;
|
||||
this.open = true;
|
||||
this.title = "修改题目";
|
||||
});
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.itemId != undefined) {
|
||||
updateQuestionnaireItem(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
} else {
|
||||
addQuestionnaireItem(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
this.open = false;
|
||||
this.getList();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const itemIds = row.itemId ? [row.itemId] : this.ids;
|
||||
this.$modal.confirm('是否确认删除选中的题目?').then(() => {
|
||||
return delQuestionnaireItem(itemIds);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
},
|
||||
/** 返回按钮操作 */
|
||||
handleBack() {
|
||||
this.$router.push('/psychology/questionnaire');
|
||||
},
|
||||
/** 管理选项 */
|
||||
handleManageOptions(row) {
|
||||
this.currentItemId = row.itemId;
|
||||
this.currentItemRow = row;
|
||||
this.optionOpen = true;
|
||||
this.loadOptions();
|
||||
},
|
||||
/** 加载选项列表 */
|
||||
loadOptions() {
|
||||
if (!this.currentItemId) {
|
||||
return;
|
||||
}
|
||||
listQuestionnaireOption(this.currentItemId).then(response => {
|
||||
this.optionList = response.data || [];
|
||||
}).catch(error => {
|
||||
console.error("加载选项列表失败:", error);
|
||||
this.$modal.msgError("加载选项列表失败");
|
||||
});
|
||||
},
|
||||
/** 新增选项 */
|
||||
handleAddOption() {
|
||||
this.optionForm = {
|
||||
itemId: this.currentItemId,
|
||||
optionCode: '',
|
||||
optionContent: '',
|
||||
optionScore: 0,
|
||||
isCorrect: '0',
|
||||
sortOrder: this.optionList.length
|
||||
};
|
||||
this.currentOptionIndex = -1;
|
||||
this.optionEditOpen = true;
|
||||
},
|
||||
/** 修改选项 */
|
||||
handleUpdateOption(row, index) {
|
||||
this.optionForm = { ...row };
|
||||
this.currentOptionIndex = index;
|
||||
this.optionEditOpen = true;
|
||||
},
|
||||
/** 删除选项 */
|
||||
handleDeleteOption(index) {
|
||||
const option = this.optionList[index];
|
||||
if (option.optionId) {
|
||||
// 已保存的选项,调用删除接口
|
||||
delQuestionnaireOption(option.optionId).then(() => {
|
||||
this.optionList.splice(index, 1);
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(error => {
|
||||
this.$modal.msgError("删除失败");
|
||||
});
|
||||
} else {
|
||||
// 未保存的选项,直接删除
|
||||
this.optionList.splice(index, 1);
|
||||
}
|
||||
},
|
||||
/** 保存选项 */
|
||||
saveOption() {
|
||||
if (!this.optionForm.optionContent || this.optionForm.optionContent.trim() === '') {
|
||||
this.$modal.msgError("选项内容不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentOptionIndex >= 0) {
|
||||
// 修改选项
|
||||
if (this.optionForm.optionId) {
|
||||
updateQuestionnaireOption(this.optionForm).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.loadOptions();
|
||||
this.optionEditOpen = false;
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "修改失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.msgError("修改失败");
|
||||
});
|
||||
} else {
|
||||
// 新增选项
|
||||
addQuestionnaireOption(this.optionForm).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.loadOptions();
|
||||
this.optionEditOpen = false;
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "新增失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.msgError("新增失败");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 新增选项
|
||||
if (this.optionForm.optionId) {
|
||||
// 已有ID,更新
|
||||
updateQuestionnaireOption(this.optionForm).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.loadOptions();
|
||||
this.optionEditOpen = false;
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "修改失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.msgError("修改失败");
|
||||
});
|
||||
} else {
|
||||
// 新选项,添加
|
||||
addQuestionnaireOption(this.optionForm).then(response => {
|
||||
if (response.code === 200) {
|
||||
// 重新加载选项列表以获取正确的ID
|
||||
this.loadOptions();
|
||||
this.optionEditOpen = false;
|
||||
this.$modal.msgSuccess("新增成功");
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "新增失败");
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.msgError("新增失败");
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
/** 提交选项 */
|
||||
submitOptions() {
|
||||
this.optionOpen = false;
|
||||
this.getList();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
145
ruoyi-ui/src/views/psychology/questionnaire/start.vue
Normal file
145
ruoyi-ui/src/views/psychology/questionnaire/start.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-card shadow="never">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>选择问卷开始答题</span>
|
||||
<el-button style="float: right;" type="text" @click="handleBack">返回</el-button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="120px">
|
||||
<el-form-item label="选择问卷" prop="questionnaireId">
|
||||
<el-select v-model="form.questionnaireId" placeholder="请选择要答题的问卷" style="width: 100%;" filterable>
|
||||
<el-option
|
||||
v-for="questionnaire in questionnaireList"
|
||||
:key="questionnaire.questionnaireId"
|
||||
:label="questionnaire.questionnaireName"
|
||||
:value="questionnaire.questionnaireId">
|
||||
<span style="float: left">{{ questionnaire.questionnaireName }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ questionnaire.itemCount || 0 }}题</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="答题人姓名" prop="respondentName">
|
||||
<el-input v-model="form.respondentName" placeholder="请输入答题人姓名(可选)" style="width: 100%;" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading">开始答题</el-button>
|
||||
<el-button @click="handleBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { startQuestionnaireAnswer } from "@/api/psychology/questionnaireAnswer";
|
||||
import { listQuestionnaire } from "@/api/psychology/questionnaire";
|
||||
|
||||
export default {
|
||||
name: "QuestionnaireStart",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
questionnaireList: [],
|
||||
form: {
|
||||
questionnaireId: undefined,
|
||||
respondentName: undefined
|
||||
},
|
||||
rules: {
|
||||
questionnaireId: [
|
||||
{ required: true, message: "请选择问卷", trigger: "change" }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// 检查URL参数中是否有questionnaireId
|
||||
const questionnaireId = this.$route.query.questionnaireId;
|
||||
if (questionnaireId) {
|
||||
this.form.questionnaireId = parseInt(questionnaireId);
|
||||
// 如果已经选择了问卷,直接开始答题(不需要填写姓名)
|
||||
this.$nextTick(() => {
|
||||
this.startAnswerDirectly();
|
||||
});
|
||||
} else {
|
||||
this.loadQuestionnaires();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 加载问卷列表 */
|
||||
loadQuestionnaires() {
|
||||
listQuestionnaire({ status: '0' }).then(response => {
|
||||
this.questionnaireList = response.rows.filter(q => q.itemCount > 0);
|
||||
}).catch(error => {
|
||||
console.error("加载问卷列表失败:", error);
|
||||
this.$message.error('加载问卷列表失败,请稍后重试');
|
||||
});
|
||||
},
|
||||
/** 提交表单 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.loading = true;
|
||||
const data = {
|
||||
questionnaireId: this.form.questionnaireId,
|
||||
respondentName: this.form.respondentName || null
|
||||
};
|
||||
|
||||
startQuestionnaireAnswer(data).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("答题已开始");
|
||||
const answerId = response.data;
|
||||
this.$router.push({ path: '/psychology/questionnaire/taking', query: { answerId: answerId } });
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error('Failed to start questionnaire answer:', error);
|
||||
this.$modal.msgError("开始答题失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 直接开始答题(从测评开始页面跳转过来时使用) */
|
||||
startAnswerDirectly() {
|
||||
if (!this.form.questionnaireId) {
|
||||
this.$modal.msgError("问卷ID不能为空");
|
||||
this.$router.push('/psychology/scale');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
const data = {
|
||||
questionnaireId: this.form.questionnaireId,
|
||||
respondentName: null // 不填写姓名
|
||||
};
|
||||
|
||||
startQuestionnaireAnswer(data).then(response => {
|
||||
if (response.code === 200) {
|
||||
this.$modal.msgSuccess("答题已开始");
|
||||
const answerId = response.data;
|
||||
this.$router.push({ path: '/psychology/questionnaire/taking', query: { answerId: answerId } });
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error('Failed to start questionnaire answer:', error);
|
||||
this.$modal.msgError("开始答题失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 返回 */
|
||||
handleBack() {
|
||||
this.$router.push('/psychology/scale');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
627
ruoyi-ui/src/views/psychology/questionnaire/taking.vue
Normal file
627
ruoyi-ui/src/views/psychology/questionnaire/taking.vue
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
<template>
|
||||
<div class="questionnaire-container">
|
||||
<!-- 顶部信息栏 -->
|
||||
<div class="questionnaire-header">
|
||||
<el-card shadow="never">
|
||||
<div class="header-content">
|
||||
<div class="title">正在答题:{{ questionnaireName }}</div>
|
||||
<div class="progress">
|
||||
<span>进度:{{ currentIndex + 1 }} / {{ totalItems }}</span>
|
||||
<el-progress :percentage="progressPercent" :stroke-width="8"></el-progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button @click="handleExit">退出</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 题目区域 -->
|
||||
<el-card shadow="never" class="question-card" v-if="currentItem">
|
||||
<div class="question-number">第 {{ currentIndex + 1 }} 题</div>
|
||||
<div class="question-content">{{ currentItem.itemContent }}</div>
|
||||
<div class="question-info" v-if="currentItem.score">
|
||||
<el-tag type="info">分值:{{ currentItem.score }}分</el-tag>
|
||||
<el-tag type="warning" v-if="currentItem.isRequired === '1'" style="margin-left: 10px;">必答题</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 单选题 (radio) -->
|
||||
<div class="options-container" v-if="currentItem.itemType === 'radio'">
|
||||
<el-radio-group v-model="selectedOption" @change="handleAnswerChange">
|
||||
<div v-for="option in currentOptions" :key="option.optionId" class="option-item">
|
||||
<el-radio :label="option.optionId">
|
||||
<span v-if="option.optionCode">{{ option.optionCode }}. </span>{{ option.optionContent }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 多选题 (checkbox) -->
|
||||
<div class="options-container" v-if="currentItem.itemType === 'checkbox'">
|
||||
<el-checkbox-group v-model="selectedOptions" @change="handleAnswerChange">
|
||||
<div v-for="option in currentOptions" :key="option.optionId" class="option-item">
|
||||
<el-checkbox :label="option.optionId">
|
||||
<span v-if="option.optionCode">{{ option.optionCode }}. </span>{{ option.optionContent }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
<!-- 判断题 (boolean) -->
|
||||
<div class="options-container" v-if="currentItem.itemType === 'boolean'">
|
||||
<el-radio-group v-model="selectedOption" @change="handleAnswerChange">
|
||||
<div v-for="option in currentOptions" :key="option.optionId" class="option-item">
|
||||
<el-radio :label="option.optionId">
|
||||
<span v-if="option.optionCode">{{ option.optionCode }}. </span>{{ option.optionContent }}
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 填空题 (input) -->
|
||||
<div class="input-container" v-if="currentItem.itemType === 'input'">
|
||||
<el-input
|
||||
v-model="answerText"
|
||||
type="text"
|
||||
placeholder="请输入答案"
|
||||
@blur="handleAnswerChange"
|
||||
@input="handleAnswerChange"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 排序题 (sort) -->
|
||||
<div class="sort-container" v-if="currentItem.itemType === 'sort'">
|
||||
<div class="sort-tip">请拖拽选项进行排序</div>
|
||||
<draggable
|
||||
v-model="sortOptions"
|
||||
:options="{ animation: 200 }"
|
||||
@end="handleSortChange"
|
||||
class="sort-list"
|
||||
>
|
||||
<div
|
||||
v-for="option in sortOptions"
|
||||
:key="option.optionId"
|
||||
class="sort-item"
|
||||
>
|
||||
<i class="el-icon-rank"></i>
|
||||
<span v-if="option.optionCode">{{ option.optionCode }}. </span>{{ option.optionContent }}
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
<!-- 计算题 (calculate) -->
|
||||
<div class="input-container" v-if="currentItem.itemType === 'calculate'">
|
||||
<el-input
|
||||
v-model="answerText"
|
||||
type="number"
|
||||
placeholder="请输入计算结果(数字)"
|
||||
@blur="handleAnswerChange"
|
||||
@input="handleAnswerChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 简答题 (text) -->
|
||||
<div class="textarea-container" v-if="currentItem.itemType === 'text'">
|
||||
<el-input
|
||||
v-model="answerText"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入答案"
|
||||
@blur="handleAnswerChange"
|
||||
:maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 问答题 (textarea) -->
|
||||
<div class="textarea-container" v-if="currentItem.itemType === 'textarea'">
|
||||
<el-input
|
||||
v-model="answerText"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入答案"
|
||||
@blur="handleAnswerChange"
|
||||
:maxlength="2000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 作文题 (essay) -->
|
||||
<div class="textarea-container" v-if="currentItem.itemType === 'essay'">
|
||||
<el-input
|
||||
v-model="answerText"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="请输入作文内容"
|
||||
@blur="handleAnswerChange"
|
||||
:maxlength="5000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<div class="navigation-buttons">
|
||||
<el-button @click="handlePrev" :disabled="currentIndex === 0">上一题</el-button>
|
||||
<el-button type="primary" @click="handleNext" :disabled="currentIndex === totalItems - 1">下一题</el-button>
|
||||
<el-button type="success" @click="handleSubmit" :disabled="loading" style="margin-left: 20px;">
|
||||
提交问卷 ({{ answeredCount }}/{{ totalItems }})
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getQuestionnaireAnswer, getQuestionnaireItems, getAnswerDetails, saveQuestionnaireAnswer, submitQuestionnaireAnswer } from "@/api/psychology/questionnaireAnswer";
|
||||
import { getQuestionnaire } from "@/api/psychology/questionnaire";
|
||||
import { listQuestionnaireOption } from "@/api/psychology/questionnaireOption";
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
export default {
|
||||
name: "QuestionnaireTaking",
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
answerId: null,
|
||||
questionnaireId: null,
|
||||
questionnaireName: '',
|
||||
itemList: [],
|
||||
optionMap: {},
|
||||
currentIndex: 0,
|
||||
selectedOption: null,
|
||||
selectedOptions: [],
|
||||
answerText: '',
|
||||
sortOptions: [],
|
||||
originalSortOptions: [],
|
||||
answersMap: {},
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentItem() {
|
||||
return this.itemList[this.currentIndex];
|
||||
},
|
||||
currentOptions() {
|
||||
return this.optionMap[this.currentItem?.itemId] || [];
|
||||
},
|
||||
totalItems() {
|
||||
return this.itemList.length;
|
||||
},
|
||||
progressPercent() {
|
||||
return this.totalItems > 0 ? Math.round((this.currentIndex + 1) / this.totalItems * 100) : 0;
|
||||
},
|
||||
answeredCount() {
|
||||
return this.itemList.filter(item => {
|
||||
const answer = this.answersMap[item.itemId];
|
||||
if (!answer) {
|
||||
return false;
|
||||
}
|
||||
// 根据题型判断是否已回答
|
||||
if (item.itemType === 'radio' || item.itemType === 'boolean') {
|
||||
return answer.optionId != null;
|
||||
} else if (item.itemType === 'checkbox' || item.itemType === 'sort') {
|
||||
return answer.optionIds != null && answer.optionIds.trim().length > 0;
|
||||
} else if (['input', 'calculate', 'text', 'textarea', 'essay'].includes(item.itemType)) {
|
||||
return answer.answerText != null && answer.answerText.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}).length;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.answerId = this.$route.query.answerId;
|
||||
if (!this.answerId) {
|
||||
this.$modal.msgError("答题ID不能为空");
|
||||
this.$router.push('/psychology/scale');
|
||||
return;
|
||||
}
|
||||
this.loadAnswer();
|
||||
},
|
||||
methods: {
|
||||
/** 加载答题信息 */
|
||||
loadAnswer() {
|
||||
this.loading = true;
|
||||
Promise.all([
|
||||
getQuestionnaireAnswer(this.answerId),
|
||||
getAnswerDetails(this.answerId)
|
||||
]).then(([answerRes, detailsRes]) => {
|
||||
const answer = answerRes.data;
|
||||
if (!answer) {
|
||||
this.$modal.msgError("答题记录不存在");
|
||||
this.$router.push('/psychology/scale');
|
||||
return;
|
||||
}
|
||||
|
||||
this.questionnaireId = answer.questionnaireId;
|
||||
|
||||
// 加载问卷信息
|
||||
getQuestionnaire(this.questionnaireId).then(response => {
|
||||
const questionnaire = response.data;
|
||||
if (questionnaire) {
|
||||
this.questionnaireName = questionnaire.questionnaireName;
|
||||
} else {
|
||||
this.questionnaireName = '未知问卷';
|
||||
}
|
||||
}).catch(() => {
|
||||
this.questionnaireName = '未知问卷';
|
||||
});
|
||||
|
||||
// 加载题目列表
|
||||
getQuestionnaireItems(this.questionnaireId).then(response => {
|
||||
this.itemList = response.data || [];
|
||||
|
||||
if (this.itemList.length === 0) {
|
||||
this.$modal.msgWarning("该问卷暂无题目,请联系管理员添加题目");
|
||||
this.$router.push('/psychology/scale');
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载已保存的答案
|
||||
const savedDetails = detailsRes.data || [];
|
||||
savedDetails.forEach(detail => {
|
||||
this.answersMap[detail.itemId] = {
|
||||
answerId: detail.answerId,
|
||||
itemId: detail.itemId,
|
||||
optionId: detail.optionId,
|
||||
optionIds: detail.optionIds,
|
||||
answerText: detail.answerText
|
||||
};
|
||||
});
|
||||
|
||||
// 加载所有题目的选项
|
||||
this.loadAllOptions().then(() => {
|
||||
this.loadCurrentAnswer();
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error('加载选项失败:', error);
|
||||
this.$modal.msgError("加载题目选项失败,请刷新重试");
|
||||
this.loading = false;
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('加载题目列表失败:', error);
|
||||
this.$modal.msgError("加载题目列表失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('加载答题信息失败:', error);
|
||||
this.$modal.msgError("加载答题信息失败,请重试");
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
/** 加载所有题目的选项 */
|
||||
loadAllOptions() {
|
||||
const promises = this.itemList.map(item => {
|
||||
// 只有需要选项的题型才加载选项
|
||||
if (['radio', 'checkbox', 'boolean', 'sort'].includes(item.itemType)) {
|
||||
return listQuestionnaireOption(item.itemId).then(response => {
|
||||
const options = response.data || [];
|
||||
options.sort((a, b) => {
|
||||
const orderA = a.sortOrder || 0;
|
||||
const orderB = b.sortOrder || 0;
|
||||
return orderA - orderB;
|
||||
});
|
||||
this.$set(this.optionMap, item.itemId, options);
|
||||
|
||||
// 如果是排序题,初始化排序选项
|
||||
if (item.itemType === 'sort') {
|
||||
this.$set(this.originalSortOptions, item.itemId, [...options]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
return Promise.all(promises);
|
||||
},
|
||||
/** 答案改变事件 */
|
||||
handleAnswerChange() {
|
||||
if (!this.currentItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = this.currentItem.itemId;
|
||||
const answer = {
|
||||
answerId: this.answerId,
|
||||
itemId: itemId,
|
||||
optionId: null,
|
||||
optionIds: null,
|
||||
answerText: null
|
||||
};
|
||||
|
||||
if (this.currentItem.itemType === 'radio' || this.currentItem.itemType === 'boolean') {
|
||||
answer.optionId = this.selectedOption;
|
||||
} else if (this.currentItem.itemType === 'checkbox') {
|
||||
answer.optionIds = this.selectedOptions.length > 0 ? this.selectedOptions.join(',') : null;
|
||||
} else if (this.currentItem.itemType === 'sort') {
|
||||
const sortIds = this.sortOptions.map(opt => opt.optionId).join(',');
|
||||
answer.optionIds = sortIds;
|
||||
} else if (['input', 'calculate', 'text', 'textarea', 'essay'].includes(this.currentItem.itemType)) {
|
||||
answer.answerText = this.answerText;
|
||||
}
|
||||
|
||||
// 保存到本地
|
||||
this.$set(this.answersMap, itemId, answer);
|
||||
|
||||
// 自动保存到服务器
|
||||
this.saveAnswerToServer(answer);
|
||||
},
|
||||
/** 排序改变事件 */
|
||||
handleSortChange() {
|
||||
this.handleAnswerChange();
|
||||
},
|
||||
/** 保存答案到服务器 */
|
||||
saveAnswerToServer(answer) {
|
||||
const data = {
|
||||
answerId: answer.answerId,
|
||||
itemId: answer.itemId,
|
||||
optionId: answer.optionId,
|
||||
optionIds: answer.optionIds,
|
||||
answerText: answer.answerText
|
||||
};
|
||||
|
||||
saveQuestionnaireAnswer(data).then(() => {
|
||||
// 静默保存成功
|
||||
}).catch(error => {
|
||||
console.error('保存答案失败:', error);
|
||||
// 不显示错误提示,避免打断用户答题
|
||||
});
|
||||
},
|
||||
/** 上一题 */
|
||||
handlePrev() {
|
||||
if (this.currentIndex > 0) {
|
||||
this.currentIndex--;
|
||||
this.loadCurrentAnswer();
|
||||
}
|
||||
},
|
||||
/** 下一题 */
|
||||
handleNext() {
|
||||
if (this.currentIndex < this.totalItems - 1) {
|
||||
this.currentIndex++;
|
||||
this.loadCurrentAnswer();
|
||||
}
|
||||
},
|
||||
/** 加载当前题目的答案 */
|
||||
loadCurrentAnswer() {
|
||||
if (!this.currentItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = this.currentItem.itemId;
|
||||
const answer = this.answersMap[itemId];
|
||||
|
||||
if (this.currentItem.itemType === 'radio' || this.currentItem.itemType === 'boolean') {
|
||||
this.selectedOption = answer && answer.optionId ? answer.optionId : null;
|
||||
this.selectedOptions = [];
|
||||
this.answerText = '';
|
||||
} else if (this.currentItem.itemType === 'checkbox') {
|
||||
this.selectedOption = null;
|
||||
if (answer && answer.optionIds) {
|
||||
this.selectedOptions = answer.optionIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
||||
} else {
|
||||
this.selectedOptions = [];
|
||||
}
|
||||
this.answerText = '';
|
||||
} else if (this.currentItem.itemType === 'sort') {
|
||||
this.selectedOption = null;
|
||||
this.selectedOptions = [];
|
||||
this.answerText = '';
|
||||
|
||||
// 加载排序题的选项
|
||||
const options = this.optionMap[itemId] || [];
|
||||
if (answer && answer.optionIds) {
|
||||
// 根据保存的顺序重新排列选项
|
||||
const savedOrder = answer.optionIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
||||
const optionMap = {};
|
||||
options.forEach(opt => {
|
||||
optionMap[opt.optionId] = opt;
|
||||
});
|
||||
|
||||
this.sortOptions = savedOrder.map(id => optionMap[id]).filter(opt => opt != null);
|
||||
// 添加未保存的选项
|
||||
savedOrder.forEach(id => {
|
||||
if (!optionMap[id]) {
|
||||
const opt = options.find(o => o.optionId === id);
|
||||
if (opt) this.sortOptions.push(opt);
|
||||
}
|
||||
});
|
||||
// 添加新选项
|
||||
options.forEach(opt => {
|
||||
if (!savedOrder.includes(opt.optionId)) {
|
||||
this.sortOptions.push(opt);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.sortOptions = [...options];
|
||||
}
|
||||
} else if (['input', 'calculate', 'text', 'textarea', 'essay'].includes(this.currentItem.itemType)) {
|
||||
this.selectedOption = null;
|
||||
this.selectedOptions = [];
|
||||
this.answerText = answer && answer.answerText ? answer.answerText : '';
|
||||
} else {
|
||||
this.selectedOption = null;
|
||||
this.selectedOptions = [];
|
||||
this.answerText = '';
|
||||
}
|
||||
},
|
||||
/** 退出 */
|
||||
handleExit() {
|
||||
this.$modal.confirm('确定要退出答题吗?已答题目将会保存。').then(() => {
|
||||
this.$router.push('/psychology/scale');
|
||||
});
|
||||
},
|
||||
/** 提交问卷 */
|
||||
handleSubmit() {
|
||||
// 检查必答题是否都已作答
|
||||
const requiredItems = this.itemList.filter(item => item.isRequired === '1');
|
||||
const unansweredRequired = requiredItems.filter(item => {
|
||||
const answer = this.answersMap[item.itemId];
|
||||
if (!answer) return true;
|
||||
|
||||
if (item.itemType === 'radio' || item.itemType === 'boolean') {
|
||||
return !answer.optionId;
|
||||
} else if (item.itemType === 'checkbox' || item.itemType === 'sort') {
|
||||
return !answer.optionIds || answer.optionIds.trim().length === 0;
|
||||
} else if (['input', 'calculate', 'text', 'textarea', 'essay'].includes(item.itemType)) {
|
||||
return !answer.answerText || answer.answerText.trim().length === 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (unansweredRequired.length > 0) {
|
||||
this.$modal.msgWarning(`还有 ${unansweredRequired.length} 道必答题未作答,请完成后再提交`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$modal.confirm('确定要提交问卷吗?提交后将不能修改。').then(() => {
|
||||
this.loading = true;
|
||||
submitQuestionnaireAnswer(this.answerId).then(response => {
|
||||
this.loading = false;
|
||||
this.$modal.msgSuccess(response.msg || "问卷已提交");
|
||||
this.$router.push('/psychology/scale');
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
this.$modal.msgError(error.msg || "提交失败,请重试");
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentIndex() {
|
||||
this.loadCurrentAnswer();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.questionnaire-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.questionnaire-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.progress span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
margin-bottom: 20px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.question-number {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.question-content {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.question-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.input-container,
|
||||
.textarea-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sort-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.sort-tip {
|
||||
margin-bottom: 15px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sort-list {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
cursor: move;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sort-item:hover {
|
||||
border-color: #409EFF;
|
||||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.sort-item i {
|
||||
margin-right: 10px;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.navigation-buttons {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
<el-descriptions title="基本信息" :column="2" border>
|
||||
<el-descriptions-item label="报告ID">{{ reportForm.reportId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="测评ID">{{ reportForm.assessmentId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源类型">
|
||||
<el-tag v-if="sourceType === 'questionnaire'" type="warning">问卷</el-tag>
|
||||
<el-tag v-else type="primary">量表</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源ID">{{ reportForm.assessmentId || reportForm.answerId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报告标题" :span="2">{{ reportForm.reportTitle || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报告类型">
|
||||
<el-tag v-if="reportForm.reportType === 'standard'" type="">标准报告</el-tag>
|
||||
|
|
@ -27,7 +31,33 @@
|
|||
<div class="summary-content" v-html="reportForm.summary || '暂无摘要'"></div>
|
||||
|
||||
<el-divider content-position="left">报告内容</el-divider>
|
||||
<div class="report-content" v-html="reportForm.reportContent || '报告内容正在生成中...'"></div>
|
||||
<div v-if="!reportForm.reportContent" style="padding: 20px; text-align: center; color: #999;">
|
||||
报告内容正在生成中...
|
||||
</div>
|
||||
<div v-else class="report-content" v-html="reportForm.reportContent"></div>
|
||||
|
||||
<!-- 问卷成绩排名 -->
|
||||
<el-divider v-if="sourceType === 'questionnaire' && reportForm.answerId" content-position="left">成绩排名</el-divider>
|
||||
<div v-if="sourceType === 'questionnaire' && reportForm.answerId" class="rank-section">
|
||||
<el-button type="primary" size="small" @click="loadRankList" :loading="rankLoading">查看排名</el-button>
|
||||
<el-table v-if="rankList.length > 0" :data="rankList" border style="width: 100%; margin-top: 15px;">
|
||||
<el-table-column type="index" label="排名" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.$index + 1 === 1" type="danger">第1名</el-tag>
|
||||
<el-tag v-else-if="scope.$index + 1 === 2" type="warning">第2名</el-tag>
|
||||
<el-tag v-else-if="scope.$index + 1 === 3" type="success">第3名</el-tag>
|
||||
<span v-else>第{{ scope.$index + 1 }}名</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="respondentName" label="答题人" width="150" />
|
||||
<el-table-column prop="totalScore" label="总分" width="120" align="center" />
|
||||
<el-table-column prop="submitTime" label="提交时间" width="180" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-divider v-if="reportForm.pdfPath" content-position="left">PDF下载</el-divider>
|
||||
<div v-if="reportForm.pdfPath">
|
||||
|
|
@ -39,13 +69,18 @@
|
|||
|
||||
<script>
|
||||
import { getReport, getReportByAssessmentId } from "@/api/psychology/report";
|
||||
import { getQuestionnaireRankList } from "@/api/psychology/questionnaireAnswer";
|
||||
import request from '@/utils/request';
|
||||
|
||||
export default {
|
||||
name: "ReportDetail",
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
reportForm: {}
|
||||
reportForm: {},
|
||||
sourceType: null,
|
||||
rankList: [],
|
||||
rankLoading: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
|
@ -57,6 +92,7 @@ export default {
|
|||
this.loading = true;
|
||||
const reportId = this.$route.query.reportId;
|
||||
const assessmentId = this.$route.query.assessmentId;
|
||||
this.sourceType = this.$route.query.sourceType;
|
||||
|
||||
if (!reportId && !assessmentId) {
|
||||
this.loading = false;
|
||||
|
|
@ -65,20 +101,41 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
const loadFunc = reportId ? getReport(reportId) : getReportByAssessmentId(assessmentId);
|
||||
|
||||
loadFunc.then(response => {
|
||||
if (response.data) {
|
||||
this.reportForm = response.data;
|
||||
} else {
|
||||
this.$modal.msgWarning("报告不存在");
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
console.error('加载报告失败:', error);
|
||||
this.$modal.msgError("加载报告失败,请检查报告是否存在");
|
||||
});
|
||||
// 如果有reportId,根据sourceType查询
|
||||
if (reportId) {
|
||||
console.log('开始加载报告,reportId:', reportId, 'sourceType:', this.sourceType);
|
||||
getReport(reportId, this.sourceType).then(response => {
|
||||
console.log('报告加载响应:', response);
|
||||
if (response && response.data) {
|
||||
console.log('报告数据:', response.data);
|
||||
console.log('报告内容:', response.data.reportContent);
|
||||
this.reportForm = response.data;
|
||||
} else {
|
||||
console.warn('报告数据为空');
|
||||
this.$modal.msgWarning("报告不存在");
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
console.error('加载报告失败:', error);
|
||||
console.error('错误详情:', error.response || error.message);
|
||||
this.$modal.msgError("加载报告失败,请检查报告是否存在");
|
||||
});
|
||||
} else {
|
||||
// 使用测评ID查询
|
||||
getReportByAssessmentId(assessmentId).then(response => {
|
||||
if (response.data) {
|
||||
this.reportForm = response.data;
|
||||
} else {
|
||||
this.$modal.msgWarning("报告不存在");
|
||||
}
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
console.error('加载报告失败:', error);
|
||||
this.$modal.msgError("加载报告失败,请检查报告是否存在");
|
||||
});
|
||||
}
|
||||
},
|
||||
/** 返回 */
|
||||
handleBack() {
|
||||
|
|
@ -91,6 +148,36 @@ export default {
|
|||
} else {
|
||||
this.$modal.msgWarning("PDF文件不存在");
|
||||
}
|
||||
},
|
||||
/** 加载排名列表 */
|
||||
loadRankList() {
|
||||
if (!this.reportForm.answerId) {
|
||||
this.$modal.msgWarning("无法获取问卷ID");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从答题记录中获取问卷ID
|
||||
request({
|
||||
url: '/psychology/questionnaire/answer/' + this.reportForm.answerId,
|
||||
method: 'get'
|
||||
}).then(response => {
|
||||
if (response.data && response.data.questionnaireId) {
|
||||
this.rankLoading = true;
|
||||
getQuestionnaireRankList(response.data.questionnaireId).then(rankResponse => {
|
||||
this.rankList = rankResponse.data || [];
|
||||
this.rankLoading = false;
|
||||
}).catch(error => {
|
||||
this.rankLoading = false;
|
||||
console.error('加载排名失败:', error);
|
||||
this.$modal.msgError("加载排名失败");
|
||||
});
|
||||
} else {
|
||||
this.$modal.msgWarning("无法获取问卷ID");
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取答题记录失败:', error);
|
||||
this.$modal.msgError("获取答题记录失败");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
|
||||
<el-form-item label="测评ID" prop="assessmentId">
|
||||
<el-input
|
||||
v-model="queryParams.assessmentId"
|
||||
placeholder="请输入测评ID"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-form-item label="来源类型" prop="sourceType">
|
||||
<el-select v-model="queryParams.sourceType" placeholder="请选择来源类型" clearable>
|
||||
<el-option label="量表" value="assessment" />
|
||||
<el-option label="问卷" value="questionnaire" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="报告类型" prop="reportType">
|
||||
<el-select v-model="queryParams.reportType" placeholder="报告类型" clearable>
|
||||
|
|
@ -29,6 +27,17 @@
|
|||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleExport"
|
||||
v-hasPermi="['psychology:report:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
|
|
@ -46,7 +55,13 @@
|
|||
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="reportId" width="80" />
|
||||
<el-table-column label="测评ID" align="center" prop="assessmentId" width="100" />
|
||||
<el-table-column label="来源类型" align="center" prop="sourceType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.sourceType === 'questionnaire'" type="warning">问卷</el-tag>
|
||||
<el-tag v-else type="primary">量表</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源ID" align="center" prop="sourceId" width="100" />
|
||||
<el-table-column label="报告标题" align="center" prop="reportTitle" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="报告类型" align="center" prop="reportType" width="120">
|
||||
<template slot-scope="scope">
|
||||
|
|
@ -110,7 +125,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { listReport, getReport, delReport } from "@/api/psychology/report";
|
||||
import { listReport, getReport, delReport, exportReport } from "@/api/psychology/report";
|
||||
|
||||
export default {
|
||||
name: "Report",
|
||||
|
|
@ -134,7 +149,7 @@ export default {
|
|||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
assessmentId: undefined,
|
||||
sourceType: undefined,
|
||||
reportType: undefined,
|
||||
isGenerated: undefined
|
||||
}
|
||||
|
|
@ -171,13 +186,59 @@ export default {
|
|||
},
|
||||
/** 查看按钮操作 */
|
||||
handleView(row) {
|
||||
this.$router.push({ path: 'report/detail', query: { reportId: row.reportId } });
|
||||
this.$router.push({ path: 'report/detail', query: { reportId: row.reportId, sourceType: row.sourceType } });
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
// TODO: 实现报告编辑功能
|
||||
this.$modal.msgInfo("报告编辑功能待实现");
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
// 如果没有选中任何报告,导出所有符合条件的报告
|
||||
const reportIds = this.ids.length > 0 ? this.ids : null;
|
||||
this.$modal.loading("正在导出报告数据...");
|
||||
exportReport(reportIds, this.queryParams).then(data => {
|
||||
// 检查返回的是否是blob数据
|
||||
if (data instanceof Blob) {
|
||||
// 直接使用blob数据
|
||||
const blob = data
|
||||
// 生成文件名
|
||||
let filename = '报告导出_' + new Date().getTime() + '.xlsx'
|
||||
if (reportIds && reportIds.length === 1) {
|
||||
// 如果是单个报告,尝试使用报告标题
|
||||
const selectedReport = this.reportList.find(report => report.reportId === reportIds[0])
|
||||
if (selectedReport && selectedReport.reportTitle) {
|
||||
filename = selectedReport.reportTitle.replace(/[^\w\s-]/g, '') + '_' + new Date().getTime() + '.xlsx'
|
||||
}
|
||||
} else if (reportIds && reportIds.length > 1) {
|
||||
filename = '报告批量导出_' + new Date().getTime() + '.xlsx'
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
this.$modal.closeLoading()
|
||||
this.$modal.msgSuccess("导出成功")
|
||||
} else {
|
||||
// 如果不是blob,可能是错误信息
|
||||
this.$modal.closeLoading()
|
||||
this.$modal.msgError("导出失败:返回数据格式错误")
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.closeLoading()
|
||||
console.error('导出失败:', error)
|
||||
const errorMsg = error.message || error.msg || "未知错误"
|
||||
this.$modal.msgError("导出失败:" + errorMsg)
|
||||
});
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const reportIds = row.reportId ? [row.reportId] : this.ids;
|
||||
|
|
|
|||
|
|
@ -1,37 +1,65 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="21.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['psychology:factor:add']"
|
||||
>新增因子</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['psychology:factor:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="el-icon-back"
|
||||
size="mini"
|
||||
@click="handleBack"
|
||||
>返回</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 量表选择界面(当没有scaleId时显示) -->
|
||||
<el-card v-if="!queryParams.scaleId" class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>请选择量表</span>
|
||||
</div>
|
||||
<el-form :inline="true" :model="scaleQuery" class="demo-form-inline">
|
||||
<el-form-item label="量表名称">
|
||||
<el-input v-model="scaleQuery.scaleName" placeholder="请输入量表名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleScaleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetScaleQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="scaleLoading" :data="scaleList" @row-click="handleScaleSelect">
|
||||
<el-table-column label="量表名称" prop="scaleName" />
|
||||
<el-table-column label="量表编码" prop="scaleCode" width="150" />
|
||||
<el-table-column label="题目数量" prop="itemCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="handleScaleSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-table v-loading="loading" :data="factorList" @selection-change="handleSelectionChange">
|
||||
<!-- 因子管理界面(当有scaleId时显示) -->
|
||||
<div v-else>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="21.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['psychology:factor:add']"
|
||||
>新增因子</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['psychology:factor:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="el-icon-back"
|
||||
size="mini"
|
||||
@click="handleBack"
|
||||
>返回</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="factorList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="因子顺序" align="center" prop="factorOrder" width="100" />
|
||||
<el-table-column label="因子编码" align="center" prop="factorCode" width="120" />
|
||||
|
|
@ -79,6 +107,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改因子对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
|
|
@ -211,6 +240,7 @@ import { listFactor, getFactor, delFactor, addFactor, updateFactor } from "@/api
|
|||
import { listItem } from "@/api/psychology/item";
|
||||
import { listFactorRule, saveRules } from "@/api/psychology/factor";
|
||||
import { listOption } from "@/api/psychology/option";
|
||||
import { listScale } from "@/api/psychology/scale";
|
||||
|
||||
export default {
|
||||
name: "ScaleFactor",
|
||||
|
|
@ -244,6 +274,13 @@ export default {
|
|||
queryParams: {
|
||||
scaleId: undefined
|
||||
},
|
||||
// 量表选择相关
|
||||
scaleLoading: false,
|
||||
scaleList: [],
|
||||
scaleQuery: {
|
||||
scaleName: undefined
|
||||
},
|
||||
currentScaleName: '',
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
|
|
@ -262,15 +299,50 @@ export default {
|
|||
const scaleName = this.$route.query.scaleName;
|
||||
if (scaleId) {
|
||||
this.queryParams.scaleId = scaleId;
|
||||
this.currentScaleName = scaleName || '';
|
||||
this.getList();
|
||||
this.loadItems();
|
||||
} else {
|
||||
// 如果没有scaleId,加载量表列表供选择
|
||||
this.loadScaleList();
|
||||
}
|
||||
// 更新页面标题
|
||||
if (scaleName) {
|
||||
document.title = scaleName + " - 因子管理";
|
||||
} else {
|
||||
document.title = "因子管理";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 加载量表列表 */
|
||||
loadScaleList() {
|
||||
this.scaleLoading = true;
|
||||
listScale(this.scaleQuery).then(response => {
|
||||
this.scaleList = response.rows || [];
|
||||
this.scaleLoading = false;
|
||||
}).catch(() => {
|
||||
this.scaleLoading = false;
|
||||
});
|
||||
},
|
||||
/** 量表搜索 */
|
||||
handleScaleQuery() {
|
||||
this.loadScaleList();
|
||||
},
|
||||
/** 重置量表搜索 */
|
||||
resetScaleQuery() {
|
||||
this.scaleQuery = {
|
||||
scaleName: undefined
|
||||
};
|
||||
this.loadScaleList();
|
||||
},
|
||||
/** 选择量表 */
|
||||
handleScaleSelect(row) {
|
||||
this.queryParams.scaleId = row.scaleId;
|
||||
this.currentScaleName = row.scaleName;
|
||||
document.title = row.scaleName + " - 因子管理";
|
||||
this.getList();
|
||||
this.loadItems();
|
||||
},
|
||||
/** 查询因子列表 */
|
||||
getList() {
|
||||
this.loading = true;
|
||||
|
|
@ -357,7 +429,18 @@ export default {
|
|||
},
|
||||
/** 返回按钮操作 */
|
||||
handleBack() {
|
||||
this.$router.push('/psychology/scale');
|
||||
if (this.queryParams.scaleId) {
|
||||
// 如果有scaleId,清除它并显示量表选择界面
|
||||
this.queryParams.scaleId = undefined;
|
||||
this.currentScaleName = '';
|
||||
this.factorList = [];
|
||||
this.itemList = [];
|
||||
this.loadScaleList();
|
||||
document.title = "因子管理";
|
||||
} else {
|
||||
// 如果没有scaleId,返回量表管理页面
|
||||
this.$router.push('/psychology/scale');
|
||||
}
|
||||
},
|
||||
/** 计分规则管理按钮操作 */
|
||||
handleManageRules(row) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,16 @@
|
|||
v-hasPermi="['psychology:scale:add']"
|
||||
>导入</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-download"
|
||||
size="mini"
|
||||
@click="handleExport"
|
||||
v-hasPermi="['psychology:scale:export']"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
|
|
@ -91,23 +101,42 @@
|
|||
|
||||
<el-table v-loading="loading" :data="scaleList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="scaleId" width="80" />
|
||||
<el-table-column label="量表编码" align="center" prop="scaleCode" width="150" />
|
||||
<el-table-column label="序号" align="center" prop="scaleId" width="80">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.sourceType === 'questionnaire'">问卷{{ Math.abs(scope.row.scaleId) }}</span>
|
||||
<span v-else>{{ scope.row.scaleId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" align="center" prop="sourceType" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.sourceType === 'questionnaire'" type="warning">问卷</el-tag>
|
||||
<el-tag v-else type="primary">量表</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="编码" align="center" prop="scaleCode" width="150" />
|
||||
<el-table-column
|
||||
label="量表名称"
|
||||
label="名称"
|
||||
align="center"
|
||||
prop="scaleName"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
label="量表英文名"
|
||||
label="英文名"
|
||||
align="center"
|
||||
prop="scaleEnName"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column label="量表类型" align="center" prop="scaleType" width="120">
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.scaleType" type="primary">
|
||||
<span v-if="scope.row.sourceType === 'questionnaire'">-</span>
|
||||
<span v-else>{{ scope.row.scaleEnName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" align="center" prop="scaleType" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.sourceType === 'questionnaire' && scope.row.scaleType" type="info">
|
||||
{{ getDictLabel(dict.type.psy_questionnaire_type, scope.row.scaleType) }}
|
||||
</el-tag>
|
||||
<el-tag v-else-if="scope.row.scaleType" type="primary">
|
||||
{{ getDictLabel(dict.type.psy_scale_type, scope.row.scaleType) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
|
|
@ -128,43 +157,84 @@
|
|||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="320">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="380">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-document"
|
||||
@click="handleManageItems(scope.row)"
|
||||
v-hasPermi="['psychology:item:list']"
|
||||
>题目管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-s-operation"
|
||||
@click="handleManageFactors(scope.row)"
|
||||
v-hasPermi="['psychology:factor:list']"
|
||||
>因子管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['psychology:scale:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['psychology:scale:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['psychology:scale:remove']"
|
||||
>删除</el-button>
|
||||
<!-- 问卷的操作按钮 -->
|
||||
<template v-if="scope.row.sourceType === 'questionnaire'">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-document"
|
||||
@click="handleManageQuestionnaireItems(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:query']"
|
||||
>题目管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleQuestionnaireDetail(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleQuestionnaireUpdate(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleQuestionnaireDelete(scope.row)"
|
||||
v-hasPermi="['psychology:questionnaire:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
<!-- 量表的操作按钮 -->
|
||||
<template v-else>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-document"
|
||||
@click="handleManageItems(scope.row)"
|
||||
v-hasPermi="['psychology:item:list']"
|
||||
>题目管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-s-operation"
|
||||
@click="handleManageFactors(scope.row)"
|
||||
v-hasPermi="['psychology:factor:list']"
|
||||
>因子管理</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row)"
|
||||
v-hasPermi="['psychology:scale:query']"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-qr-code"
|
||||
@click="handleGenerateQrcode(scope.row)"
|
||||
v-hasPermi="['psychology:qrcode:add']"
|
||||
>二维码</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['psychology:scale:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['psychology:scale:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -321,15 +391,53 @@
|
|||
<el-button @click="cancelImport">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 二维码对话框 -->
|
||||
<el-dialog title="量表测评二维码" :visible.sync="qrcodeOpen" width="500px" append-to-body>
|
||||
<div v-if="qrcodeInfo && qrcodeInfo.qrcodeUrl" style="text-align: center;">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<img
|
||||
:src="qrcodeInfo.qrcodeUrl"
|
||||
alt="二维码"
|
||||
style="max-width: 300px; max-height: 300px; border: 1px solid #dcdfe6; padding: 10px; background: #fff;"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<p style="color: #606266; font-size: 14px;">扫码即可开始测评</p>
|
||||
<p style="color: #909399; font-size: 12px; margin-top: 10px;">扫码次数: {{ qrcodeInfo.scanCount || 0 }}</p>
|
||||
<p style="color: #909399; font-size: 12px; margin-top: 5px;">二维码编码: {{ qrcodeInfo.qrcodeCode }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="downloadQrcode">下载二维码</el-button>
|
||||
<el-button @click="qrcodeOpen = false">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="qrcodeInfo && !qrcodeInfo.qrcodeUrl" style="text-align: center; padding: 40px;">
|
||||
<el-alert
|
||||
title="二维码生成失败,请重试"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon>
|
||||
</el-alert>
|
||||
<el-button style="margin-top: 20px;" @click="qrcodeOpen = false">关闭</el-button>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 40px;">
|
||||
<i class="el-icon-loading" style="font-size: 48px; color: #409EFF;"></i>
|
||||
<p style="margin-top: 20px; color: #606266;">正在生成二维码...</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listScale, getScale, delScale, addScale, updateScale, importScale, importScaleFile, previewDocument } from "@/api/psychology/scale"
|
||||
import { listScale, getScale, delScale, addScale, updateScale, importScale, importScaleFile, previewDocument, exportScale } from "@/api/psychology/scale"
|
||||
import { getQuestionnaire, delQuestionnaire, updateQuestionnaire } from "@/api/psychology/questionnaire"
|
||||
import { generateScaleQrcode } from "@/api/psychology/qrcode"
|
||||
|
||||
export default {
|
||||
name: "PsyScale",
|
||||
dicts: ['psy_scale_status', 'psy_scale_type'],
|
||||
dicts: ['psy_scale_status', 'psy_scale_type', 'psy_questionnaire_type'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
|
@ -363,6 +471,9 @@ export default {
|
|||
// 文件内容
|
||||
fileContent: null
|
||||
},
|
||||
// 二维码对话框
|
||||
qrcodeOpen: false,
|
||||
qrcodeInfo: null,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
|
|
@ -521,6 +632,52 @@ export default {
|
|||
this.$modal.msgSuccess("删除成功")
|
||||
}).catch(() => {})
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
// 如果没有选中任何量表,导出所有量表
|
||||
const scaleIds = this.ids.length > 0 ? this.ids : null
|
||||
this.$modal.loading("正在导出量表数据...")
|
||||
exportScale(scaleIds).then(data => {
|
||||
// 检查返回的是否是blob数据
|
||||
if (data instanceof Blob) {
|
||||
// 直接使用blob数据
|
||||
const blob = data
|
||||
// 生成文件名
|
||||
let filename = '量表导出_' + new Date().getTime() + '.json'
|
||||
if (scaleIds && scaleIds.length === 1) {
|
||||
// 如果是单个量表,尝试使用量表名称
|
||||
const selectedScale = this.scaleList.find(scale => scale.scaleId === scaleIds[0])
|
||||
if (selectedScale && selectedScale.scaleName) {
|
||||
filename = selectedScale.scaleName + '_' + new Date().getTime() + '.json'
|
||||
}
|
||||
} else if (scaleIds && scaleIds.length > 1) {
|
||||
filename = '量表批量导出_' + new Date().getTime() + '.json'
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
this.$modal.closeLoading()
|
||||
this.$modal.msgSuccess("导出成功")
|
||||
} else {
|
||||
// 如果不是blob,可能是错误信息
|
||||
this.$modal.closeLoading()
|
||||
this.$modal.msgError("导出失败:返回数据格式错误")
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$modal.closeLoading()
|
||||
console.error('导出失败:', error)
|
||||
const errorMsg = error.message || error.msg || "未知错误"
|
||||
this.$modal.msgError("导出失败:" + errorMsg)
|
||||
})
|
||||
},
|
||||
/** 题目管理按钮操作 */
|
||||
handleManageItems(row) {
|
||||
const scaleId = row.scaleId
|
||||
|
|
@ -531,6 +688,68 @@ export default {
|
|||
const scaleId = row.scaleId
|
||||
this.$router.push({ path: '/psychology/scale/factor', query: { scaleId: scaleId, scaleName: row.scaleName } })
|
||||
},
|
||||
/** 生成二维码按钮操作 */
|
||||
handleGenerateQrcode(row) {
|
||||
const scaleId = row.scaleId
|
||||
this.qrcodeInfo = null
|
||||
this.qrcodeOpen = true
|
||||
|
||||
generateScaleQrcode(scaleId).then(response => {
|
||||
if (response.code === 200 && response.data) {
|
||||
this.qrcodeInfo = response.data
|
||||
// 检查二维码URL是否存在
|
||||
if (!this.qrcodeInfo.qrcodeUrl) {
|
||||
this.$modal.msgError("二维码图片生成失败,请重试")
|
||||
}
|
||||
} else {
|
||||
this.$modal.msgError(response.msg || "生成二维码失败")
|
||||
this.qrcodeOpen = false
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('生成二维码失败:', error)
|
||||
this.$modal.msgError(error.msg || error.message || "生成二维码失败")
|
||||
this.qrcodeOpen = false
|
||||
})
|
||||
},
|
||||
/** 图片加载错误处理 */
|
||||
handleImageError(event) {
|
||||
console.error('二维码图片加载失败:', event)
|
||||
this.$modal.msgError("二维码图片加载失败")
|
||||
},
|
||||
/** 下载二维码 */
|
||||
downloadQrcode() {
|
||||
if (!this.qrcodeInfo || !this.qrcodeInfo.qrcodeUrl) {
|
||||
this.$modal.msgWarning("二维码图片不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是Base64格式,直接下载
|
||||
if (this.qrcodeInfo.qrcodeUrl.startsWith('data:image')) {
|
||||
const link = document.createElement('a')
|
||||
link.href = this.qrcodeInfo.qrcodeUrl
|
||||
link.download = `量表二维码_${this.qrcodeInfo.qrcodeCode}.png`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} else {
|
||||
// 如果是URL格式,使用fetch下载
|
||||
fetch(this.qrcodeInfo.qrcodeUrl)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `量表二维码_${this.qrcodeInfo.qrcodeCode}.png`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
.catch(error => {
|
||||
this.$modal.msgError("下载失败:" + (error.message || "未知错误"))
|
||||
})
|
||||
}
|
||||
},
|
||||
/** 导入按钮操作 */
|
||||
handleImport() {
|
||||
this.importOpen = true
|
||||
|
|
@ -680,6 +899,36 @@ export default {
|
|||
this.upload.fileContent = null
|
||||
}
|
||||
}
|
||||
},
|
||||
/** 问卷题目管理按钮操作 */
|
||||
handleManageQuestionnaireItems(row) {
|
||||
const questionnaireId = row.originalId || Math.abs(row.scaleId)
|
||||
// 跳转到问卷题目管理页面(如果存在)或问卷管理页面
|
||||
this.$router.push({ path: '/psychology/questionnaire', query: { questionnaireId: questionnaireId } })
|
||||
},
|
||||
/** 问卷详情按钮操作 */
|
||||
handleQuestionnaireDetail(row) {
|
||||
const questionnaireId = row.originalId || Math.abs(row.scaleId)
|
||||
getQuestionnaire(questionnaireId).then(response => {
|
||||
this.$modal.msgInfo("问卷详情:" + JSON.stringify(response.data, null, 2))
|
||||
})
|
||||
},
|
||||
/** 问卷修改按钮操作 */
|
||||
handleQuestionnaireUpdate(row) {
|
||||
const questionnaireId = row.originalId || Math.abs(row.scaleId)
|
||||
// 跳转到问卷管理页面进行编辑
|
||||
this.$router.push({ path: '/psychology/questionnaire', query: { questionnaireId: questionnaireId, action: 'edit' } })
|
||||
},
|
||||
/** 问卷删除按钮操作 */
|
||||
handleQuestionnaireDelete(row) {
|
||||
const questionnaireId = row.originalId || Math.abs(row.scaleId)
|
||||
const questionnaireName = row.scaleName
|
||||
this.$modal.confirm('是否确认删除问卷"' + questionnaireName + '"的数据项?').then(() => {
|
||||
return delQuestionnaire(questionnaireId)
|
||||
}).then(() => {
|
||||
this.getList()
|
||||
this.$modal.msgSuccess("删除成功")
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,65 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="21.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['psychology:item:add']"
|
||||
>新增题目</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['psychology:item:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="el-icon-back"
|
||||
size="mini"
|
||||
@click="handleBack"
|
||||
>返回</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 量表选择界面(当没有scaleId时显示) -->
|
||||
<el-card v-if="!queryParams.scaleId" class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>请选择量表</span>
|
||||
</div>
|
||||
<el-form :inline="true" :model="scaleQuery" class="demo-form-inline">
|
||||
<el-form-item label="量表名称">
|
||||
<el-input v-model="scaleQuery.scaleName" placeholder="请输入量表名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleScaleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetScaleQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="scaleLoading" :data="scaleList" @row-click="handleScaleSelect">
|
||||
<el-table-column label="量表名称" prop="scaleName" />
|
||||
<el-table-column label="量表编码" prop="scaleCode" width="150" />
|
||||
<el-table-column label="题目数量" prop="itemCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click.stop="handleScaleSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-table v-loading="loading" :data="itemList" @selection-change="handleSelectionChange">
|
||||
<!-- 题目管理界面(当有scaleId时显示) -->
|
||||
<div v-else>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="21.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-hasPermi="['psychology:item:add']"
|
||||
>新增题目</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-delete"
|
||||
size="mini"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete"
|
||||
v-hasPermi="['psychology:item:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-button
|
||||
type="info"
|
||||
plain
|
||||
icon="el-icon-back"
|
||||
size="mini"
|
||||
@click="handleBack"
|
||||
>返回</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="itemList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="itemNumber" width="80" />
|
||||
<el-table-column
|
||||
|
|
@ -81,6 +109,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改题目对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
|
||||
|
|
@ -197,6 +226,13 @@ export default {
|
|||
queryParams: {
|
||||
scaleId: undefined
|
||||
},
|
||||
// 量表选择相关
|
||||
scaleLoading: false,
|
||||
scaleList: [],
|
||||
scaleQuery: {
|
||||
scaleName: undefined
|
||||
},
|
||||
currentScaleName: '',
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
|
|
@ -218,19 +254,61 @@ export default {
|
|||
const scaleName = this.$route.query.scaleName;
|
||||
if (scaleId) {
|
||||
this.queryParams.scaleId = scaleId;
|
||||
this.currentScaleName = scaleName || '';
|
||||
this.getList();
|
||||
} else {
|
||||
// 如果没有scaleId,加载量表列表供选择
|
||||
this.loadScales();
|
||||
}
|
||||
// 更新页面标题
|
||||
if (scaleName) {
|
||||
document.title = scaleName + " - 题目管理";
|
||||
} else {
|
||||
document.title = "题目管理";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 加载量表列表 */
|
||||
loadScales() {
|
||||
this.scaleLoading = true;
|
||||
listScale(this.scaleQuery).then(response => {
|
||||
this.scaleList = response.rows || [];
|
||||
this.scaleLoading = false;
|
||||
}).catch(() => {
|
||||
this.scaleLoading = false;
|
||||
});
|
||||
},
|
||||
/** 搜索量表 */
|
||||
handleScaleQuery() {
|
||||
this.loadScales();
|
||||
},
|
||||
/** 重置量表搜索 */
|
||||
resetScaleQuery() {
|
||||
this.scaleQuery = {
|
||||
scaleName: undefined
|
||||
};
|
||||
this.loadScales();
|
||||
},
|
||||
/** 选择量表 */
|
||||
handleScaleSelect(row) {
|
||||
this.queryParams.scaleId = row.scaleId;
|
||||
this.currentScaleName = row.scaleName;
|
||||
document.title = row.scaleName + " - 题目管理";
|
||||
this.getList();
|
||||
},
|
||||
/** 查询题目列表 */
|
||||
getList() {
|
||||
if (!this.queryParams.scaleId) {
|
||||
this.$modal.msgError("请先选择量表");
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
listItem(this.queryParams.scaleId).then(response => {
|
||||
this.itemList = response.data;
|
||||
this.itemList = response.data || [];
|
||||
this.loading = false;
|
||||
}).catch(error => {
|
||||
console.error("查询题目列表失败:", error);
|
||||
this.$modal.msgError("查询题目列表失败:" + (error.message || "未知错误"));
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
|
@ -307,7 +385,17 @@ export default {
|
|||
},
|
||||
/** 返回按钮操作 */
|
||||
handleBack() {
|
||||
this.$router.push('/psychology/scale');
|
||||
if (this.queryParams.scaleId) {
|
||||
// 如果有scaleId,清除它并显示量表选择界面
|
||||
this.queryParams.scaleId = undefined;
|
||||
this.currentScaleName = '';
|
||||
this.itemList = [];
|
||||
this.loadScales();
|
||||
document.title = "题目管理";
|
||||
} else {
|
||||
// 如果没有scaleId,返回量表管理页面
|
||||
this.$router.push('/psychology/scale');
|
||||
}
|
||||
},
|
||||
/** 题型名称 */
|
||||
getItemTypeName(type) {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,26 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.core.page.TableDataInfo;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.common.utils.poi.ExcelUtil;
|
||||
import com.ddnai.system.domain.psychology.PsyAssessmentReport;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireReport;
|
||||
import com.ddnai.system.domain.psychology.vo.ReportExportVO;
|
||||
import com.ddnai.system.service.psychology.IPsyAssessmentReportService;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireReportMapper;
|
||||
import com.ddnai.common.core.page.PageDomain;
|
||||
import com.ddnai.common.core.page.TableSupport;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 测评报告 信息操作处理
|
||||
|
|
@ -30,27 +42,172 @@ public class PsyAssessmentReportController extends BaseController
|
|||
{
|
||||
@Autowired
|
||||
private IPsyAssessmentReportService reportService;
|
||||
|
||||
@Autowired
|
||||
private PsyQuestionnaireReportMapper questionnaireReportMapper;
|
||||
|
||||
/**
|
||||
* 获取报告列表
|
||||
* 获取报告列表(包含测评报告和问卷报告)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:report:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PsyAssessmentReport report)
|
||||
{
|
||||
startPage();
|
||||
List<PsyAssessmentReport> list = reportService.selectReportList(report);
|
||||
return getDataTable(list);
|
||||
System.out.println("开始查询报告列表");
|
||||
// 由于需要合并两种报告,需要手动分页,所以先清理分页参数
|
||||
clearPage();
|
||||
|
||||
// 查询测评报告(不使用分页,获取全部数据)
|
||||
List<PsyAssessmentReport> assessmentReports = reportService.selectReportList(report);
|
||||
System.out.println("查询到测评报告数量: " + (assessmentReports != null ? assessmentReports.size() : 0));
|
||||
|
||||
// 查询问卷报告(不使用分页,获取全部数据)
|
||||
PsyQuestionnaireReport questionnaireReport = new PsyQuestionnaireReport();
|
||||
if (report.getReportType() != null && !report.getReportType().isEmpty()) {
|
||||
questionnaireReport.setReportType(report.getReportType());
|
||||
}
|
||||
if (report.getIsGenerated() != null && !report.getIsGenerated().isEmpty()) {
|
||||
questionnaireReport.setIsGenerated(report.getIsGenerated());
|
||||
}
|
||||
List<PsyQuestionnaireReport> questionnaireReports = questionnaireReportMapper.selectReportList(questionnaireReport);
|
||||
System.out.println("查询到问卷报告数量: " + (questionnaireReports != null ? questionnaireReports.size() : 0));
|
||||
|
||||
// 合并报告列表(转换为统一的VO格式)
|
||||
List<ReportVO> allReports = new ArrayList<>();
|
||||
|
||||
// 添加测评报告
|
||||
for (PsyAssessmentReport ar : assessmentReports) {
|
||||
ReportVO vo = new ReportVO();
|
||||
vo.setReportId(ar.getReportId());
|
||||
vo.setSourceType("assessment");
|
||||
vo.setSourceId(ar.getAssessmentId());
|
||||
vo.setReportTitle(ar.getReportTitle());
|
||||
vo.setReportType(ar.getReportType());
|
||||
vo.setReportContent(ar.getReportContent());
|
||||
vo.setSummary(ar.getSummary());
|
||||
vo.setIsGenerated(ar.getIsGenerated());
|
||||
vo.setGenerateTime(ar.getGenerateTime());
|
||||
vo.setCreateTime(ar.getCreateTime());
|
||||
allReports.add(vo);
|
||||
}
|
||||
|
||||
// 添加问卷报告
|
||||
for (PsyQuestionnaireReport qr : questionnaireReports) {
|
||||
ReportVO vo = new ReportVO();
|
||||
vo.setReportId(qr.getReportId());
|
||||
vo.setSourceType("questionnaire");
|
||||
vo.setSourceId(qr.getAnswerId());
|
||||
vo.setReportTitle(qr.getReportTitle());
|
||||
vo.setReportType(qr.getReportType());
|
||||
vo.setReportContent(qr.getReportContent());
|
||||
vo.setSummary(qr.getSummary());
|
||||
vo.setIsGenerated(qr.getIsGenerated());
|
||||
vo.setGenerateTime(qr.getGenerateTime());
|
||||
vo.setCreateTime(qr.getCreateTime());
|
||||
allReports.add(vo);
|
||||
System.out.println("添加问卷报告: reportId=" + qr.getReportId() + ", answerId=" + qr.getAnswerId() + ", title=" + qr.getReportTitle());
|
||||
}
|
||||
|
||||
System.out.println("合并后总报告数: " + allReports.size());
|
||||
|
||||
// 按创建时间倒序排序
|
||||
allReports.sort((a, b) -> {
|
||||
if (a.getCreateTime() == null && b.getCreateTime() == null) return 0;
|
||||
if (a.getCreateTime() == null) return 1;
|
||||
if (b.getCreateTime() == null) return -1;
|
||||
return b.getCreateTime().compareTo(a.getCreateTime());
|
||||
});
|
||||
|
||||
// 手动分页
|
||||
PageDomain pageDomain = TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() != null ? pageDomain.getPageNum() : 1;
|
||||
int pageSize = pageDomain.getPageSize() != null ? pageDomain.getPageSize() : 10;
|
||||
int total = allReports.size();
|
||||
int start = (pageNum - 1) * pageSize;
|
||||
int end = Math.min(start + pageSize, total);
|
||||
|
||||
System.out.println("分页参数: pageNum=" + pageNum + ", pageSize=" + pageSize + ", total=" + total + ", start=" + start + ", end=" + end);
|
||||
|
||||
List<ReportVO> pagedList = start < total ? allReports.subList(start, end) : new ArrayList<>();
|
||||
|
||||
System.out.println("返回分页数据数量: " + pagedList.size());
|
||||
|
||||
TableDataInfo dataTable = new TableDataInfo();
|
||||
dataTable.setCode(200);
|
||||
dataTable.setMsg("查询成功");
|
||||
dataTable.setRows(pagedList);
|
||||
dataTable.setTotal(total);
|
||||
return dataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的报告VO类
|
||||
*/
|
||||
public static class ReportVO {
|
||||
private Long reportId;
|
||||
private String sourceType; // "assessment" 或 "questionnaire"
|
||||
private Long sourceId; // assessmentId 或 answerId
|
||||
private String reportTitle;
|
||||
private String reportType;
|
||||
private String reportContent;
|
||||
private String summary;
|
||||
private String isGenerated;
|
||||
private java.util.Date generateTime;
|
||||
private java.util.Date createTime;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getReportId() { return reportId; }
|
||||
public void setReportId(Long reportId) { this.reportId = reportId; }
|
||||
public String getSourceType() { return sourceType; }
|
||||
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
|
||||
public Long getSourceId() { return sourceId; }
|
||||
public void setSourceId(Long sourceId) { this.sourceId = sourceId; }
|
||||
public String getReportTitle() { return reportTitle; }
|
||||
public void setReportTitle(String reportTitle) { this.reportTitle = reportTitle; }
|
||||
public String getReportType() { return reportType; }
|
||||
public void setReportType(String reportType) { this.reportType = reportType; }
|
||||
public String getReportContent() { return reportContent; }
|
||||
public void setReportContent(String reportContent) { this.reportContent = reportContent; }
|
||||
public String getSummary() { return summary; }
|
||||
public void setSummary(String summary) { this.summary = summary; }
|
||||
public String getIsGenerated() { return isGenerated; }
|
||||
public void setIsGenerated(String isGenerated) { this.isGenerated = isGenerated; }
|
||||
public java.util.Date getGenerateTime() { return generateTime; }
|
||||
public void setGenerateTime(java.util.Date generateTime) { this.generateTime = generateTime; }
|
||||
public java.util.Date getCreateTime() { return createTime; }
|
||||
public void setCreateTime(java.util.Date createTime) { this.createTime = createTime; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据报告ID获取详细信息
|
||||
* 根据报告ID获取详细信息(支持测评报告和问卷报告)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:report:query')")
|
||||
@GetMapping(value = "/{reportId}")
|
||||
public AjaxResult getInfo(@PathVariable Long reportId)
|
||||
public AjaxResult getInfo(@PathVariable Long reportId, @RequestParam(required = false) String sourceType)
|
||||
{
|
||||
return success(reportService.selectReportById(reportId));
|
||||
System.out.println("查询报告详情,reportId: " + reportId + ", sourceType: " + sourceType);
|
||||
|
||||
// 如果指定了sourceType,根据类型查询
|
||||
if ("questionnaire".equals(sourceType)) {
|
||||
PsyQuestionnaireReport report = questionnaireReportMapper.selectReportById(reportId);
|
||||
System.out.println("查询问卷报告结果: " + (report != null ? "找到报告,reportId=" + report.getReportId() + ", answerId=" + report.getAnswerId() : "未找到"));
|
||||
if (report != null) {
|
||||
System.out.println("报告内容长度: " + (report.getReportContent() != null ? report.getReportContent().length() : 0));
|
||||
}
|
||||
return success(report);
|
||||
} else {
|
||||
// 默认查询测评报告,如果不存在则查询问卷报告
|
||||
PsyAssessmentReport report = reportService.selectReportById(reportId);
|
||||
if (report != null) {
|
||||
System.out.println("查询测评报告结果: 找到报告,reportId=" + report.getReportId());
|
||||
return success(report);
|
||||
}
|
||||
// 尝试查询问卷报告
|
||||
System.out.println("测评报告不存在,尝试查询问卷报告");
|
||||
PsyQuestionnaireReport qReport = questionnaireReportMapper.selectReportById(reportId);
|
||||
System.out.println("查询问卷报告结果: " + (qReport != null ? "找到报告" : "未找到"));
|
||||
return success(qReport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,5 +273,117 @@ public class PsyAssessmentReportController extends BaseController
|
|||
return error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出报告(Excel格式)
|
||||
* 支持单个或批量导出
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:report:export')")
|
||||
@Log(title = "测评报告", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void exportReports(@RequestParam(value = "reportIds", required = false) String reportIdsStr,
|
||||
HttpServletResponse response, PsyAssessmentReport report)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<PsyAssessmentReport> reportList;
|
||||
|
||||
// 如果指定了reportIds,导出指定的报告
|
||||
if (reportIdsStr != null && !reportIdsStr.trim().isEmpty())
|
||||
{
|
||||
// 解析reportIds字符串(逗号分隔)
|
||||
String[] reportIdStrs = reportIdsStr.split(",");
|
||||
reportList = new ArrayList<>();
|
||||
for (String reportIdStr : reportIdStrs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Long reportId = Long.parseLong(reportIdStr.trim());
|
||||
PsyAssessmentReport r = reportService.selectReportById(reportId);
|
||||
if (r != null)
|
||||
{
|
||||
reportList.add(r);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// 跳过无效的ID
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果没有指定reportIds,根据查询条件导出
|
||||
else
|
||||
{
|
||||
reportList = reportService.selectReportList(report);
|
||||
}
|
||||
|
||||
if (reportList == null || reportList.isEmpty())
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"error\":\"没有可导出的报告数据\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换为导出VO
|
||||
List<ReportExportVO> exportList = new ArrayList<>();
|
||||
Pattern htmlTagPattern = Pattern.compile("<[^>]+>", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
for (PsyAssessmentReport r : reportList)
|
||||
{
|
||||
ReportExportVO exportVO = new ReportExportVO();
|
||||
exportVO.setReportId(r.getReportId());
|
||||
exportVO.setAssessmentId(r.getAssessmentId());
|
||||
exportVO.setReportTitle(r.getReportTitle());
|
||||
exportVO.setReportType(r.getReportType());
|
||||
exportVO.setSummary(r.getSummary());
|
||||
exportVO.setIsGenerated(r.getIsGenerated());
|
||||
exportVO.setGenerateTime(r.getGenerateTime());
|
||||
exportVO.setCreateTime(r.getCreateTime());
|
||||
|
||||
// 将HTML内容转换为纯文本
|
||||
String reportContent = r.getReportContent();
|
||||
if (reportContent != null && !reportContent.isEmpty())
|
||||
{
|
||||
// 去除HTML标签
|
||||
String plainText = htmlTagPattern.matcher(reportContent).replaceAll("");
|
||||
// 替换HTML实体
|
||||
plainText = plainText.replace(" ", " ")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("&", "&")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'");
|
||||
// 去除多余空白
|
||||
plainText = plainText.replaceAll("\\s+", " ").trim();
|
||||
exportVO.setReportContentText(plainText);
|
||||
}
|
||||
else
|
||||
{
|
||||
exportVO.setReportContentText("报告内容为空");
|
||||
}
|
||||
|
||||
exportList.add(exportVO);
|
||||
}
|
||||
|
||||
// 导出Excel
|
||||
ExcelUtil<ReportExportVO> util = new ExcelUtil<ReportExportVO>(ReportExportVO.class);
|
||||
util.exportExcel(response, exportList, "测评报告数据");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"error\":\"导出失败:" + e.getMessage().replace("\"", "\\\"") + "\"}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略写入错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ddnai.web.controller.psychology;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
|
@ -13,11 +14,13 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ddnai.common.annotation.Anonymous;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.core.page.TableDataInfo;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.common.utils.StringUtils;
|
||||
import com.ddnai.common.utils.poi.ExcelUtil;
|
||||
import com.ddnai.system.domain.psychology.PsyQrcode;
|
||||
import com.ddnai.system.service.psychology.IPsyQrcodeService;
|
||||
|
|
@ -44,8 +47,39 @@ public class PsyQrcodeController extends BaseController
|
|||
{
|
||||
startPage();
|
||||
List<PsyQrcode> list = qrcodeService.selectQrcodeList(qrcode);
|
||||
// 列表不生成Base64数据,只在查看详情或需要显示时生成,以提升性能
|
||||
// 前端可以通过专门的接口获取二维码Base64数据
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二维码Base64图片(用于列表显示)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:qrcode:query')")
|
||||
@GetMapping("/image/{qrcodeId}")
|
||||
public AjaxResult getQrcodeImage(@PathVariable Long qrcodeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
PsyQrcode qrcode = qrcodeService.selectQrcodeById(qrcodeId);
|
||||
if (qrcode == null)
|
||||
{
|
||||
return error("二维码不存在");
|
||||
}
|
||||
|
||||
// 动态生成Base64
|
||||
enrichQrcodeWithBase64(qrcode);
|
||||
|
||||
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("qrcodeUrl", qrcode.getQrcodeUrl());
|
||||
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("获取二维码图片失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出二维码列表
|
||||
|
|
@ -67,7 +101,33 @@ public class PsyQrcodeController extends BaseController
|
|||
@GetMapping(value = "/{qrcodeId}")
|
||||
public AjaxResult getInfo(@PathVariable Long qrcodeId)
|
||||
{
|
||||
return success(qrcodeService.selectQrcodeById(qrcodeId));
|
||||
PsyQrcode qrcode = qrcodeService.selectQrcodeById(qrcodeId);
|
||||
if (qrcode != null)
|
||||
{
|
||||
enrichQrcodeWithBase64(qrcode);
|
||||
}
|
||||
return success(qrcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为二维码对象添加Base64图片数据
|
||||
*
|
||||
* @param qrcode 二维码对象
|
||||
*/
|
||||
private void enrichQrcodeWithBase64(PsyQrcode qrcode)
|
||||
{
|
||||
if (qrcode != null)
|
||||
{
|
||||
String qrcodeBase64 = qrcodeService.generateQrcode(qrcode);
|
||||
if (StringUtils.isNotEmpty(qrcodeBase64))
|
||||
{
|
||||
qrcode.setQrcodeUrl("data:image/png;base64," + qrcodeBase64);
|
||||
}
|
||||
else
|
||||
{
|
||||
qrcode.setQrcodeUrl("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,12 +138,21 @@ public class PsyQrcodeController extends BaseController
|
|||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PsyQrcode qrcode)
|
||||
{
|
||||
if (qrcode.getQrcodeCode() != null && !qrcodeService.checkQrcodeCodeUnique(qrcode.getQrcodeCode()))
|
||||
{
|
||||
return error("新增二维码'" + qrcode.getQrcodeCode() + "'失败,二维码编码已存在");
|
||||
}
|
||||
// 二维码编码可以为空,Service层会自动生成
|
||||
// 如果用户提供了编码,Service层会检查唯一性,如果已存在则自动生成新的
|
||||
// 删除的二维码编码会自动释放,可以重新使用
|
||||
qrcode.setCreateBy(getUsername());
|
||||
return toAjax(qrcodeService.insertQrcode(qrcode));
|
||||
int result = qrcodeService.insertQrcode(qrcode);
|
||||
if (result > 0 && qrcode.getQrcodeId() != null)
|
||||
{
|
||||
// 重新查询获取完整信息(包括生成的二维码编码)
|
||||
PsyQrcode savedQrcode = qrcodeService.selectQrcodeById(qrcode.getQrcodeId());
|
||||
if (savedQrcode != null)
|
||||
{
|
||||
return success(savedQrcode);
|
||||
}
|
||||
}
|
||||
return toAjax(result);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -94,6 +163,14 @@ public class PsyQrcodeController extends BaseController
|
|||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody PsyQrcode qrcode)
|
||||
{
|
||||
// 编辑时检查编码唯一性(排除自己)
|
||||
if (qrcode.getQrcodeCode() != null && qrcode.getQrcodeId() != null)
|
||||
{
|
||||
if (!qrcodeService.checkQrcodeCodeUnique(qrcode.getQrcodeCode(), qrcode.getQrcodeId()))
|
||||
{
|
||||
return error("修改二维码失败,二维码编码'" + qrcode.getQrcodeCode() + "'已存在");
|
||||
}
|
||||
}
|
||||
qrcode.setUpdateBy(getUsername());
|
||||
return toAjax(qrcodeService.updateQrcode(qrcode));
|
||||
}
|
||||
|
|
@ -125,17 +202,283 @@ public class PsyQrcodeController extends BaseController
|
|||
return error("二维码不存在");
|
||||
}
|
||||
|
||||
// 重新生成二维码
|
||||
String qrcodeUrl = qrcodeService.generateQrcode(qrcode);
|
||||
// 动态生成Base64(不存储到数据库)
|
||||
enrichQrcodeWithBase64(qrcode);
|
||||
|
||||
AjaxResult result = AjaxResult.success();
|
||||
result.put("qrcodeBase64", qrcodeUrl);
|
||||
return result;
|
||||
return success(qrcode);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("重新生成二维码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描二维码(公开接口,不需要权限验证)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @return 二维码信息和跳转地址
|
||||
*/
|
||||
@Anonymous
|
||||
@GetMapping("/scan/{qrcodeCode}")
|
||||
public AjaxResult scan(@PathVariable String qrcodeCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 扫描二维码(增加扫码次数)
|
||||
PsyQrcode qrcode = qrcodeService.scanQrcode(qrcodeCode);
|
||||
|
||||
if (qrcode == null)
|
||||
{
|
||||
return error("二维码不存在或已失效");
|
||||
}
|
||||
|
||||
// 检查二维码状态
|
||||
if (!"0".equals(qrcode.getStatus()))
|
||||
{
|
||||
return error("二维码已失效或已过期");
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (qrcode.getExpireTime() != null && qrcode.getExpireTime().before(new Date()))
|
||||
{
|
||||
return error("二维码已过期");
|
||||
}
|
||||
|
||||
// 构建跳转地址
|
||||
String redirectUrl = buildRedirectUrl(qrcode);
|
||||
|
||||
// 只返回必要的跳转信息,不返回Base64数据以提升性能
|
||||
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||
// 创建一个简化的二维码信息对象,不包含Base64数据
|
||||
java.util.Map<String, Object> qrcodeInfo = new java.util.HashMap<>();
|
||||
qrcodeInfo.put("qrcodeId", qrcode.getQrcodeId());
|
||||
qrcodeInfo.put("qrcodeCode", qrcode.getQrcodeCode());
|
||||
qrcodeInfo.put("qrcodeType", qrcode.getQrcodeType());
|
||||
qrcodeInfo.put("targetType", qrcode.getTargetType());
|
||||
qrcodeInfo.put("targetId", qrcode.getTargetId());
|
||||
qrcodeInfo.put("scanCount", qrcode.getScanCount());
|
||||
data.put("qrcode", qrcodeInfo);
|
||||
data.put("redirectUrl", redirectUrl);
|
||||
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("扫描二维码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建跳转地址
|
||||
*
|
||||
* @param qrcode 二维码信息
|
||||
* @return 跳转地址
|
||||
*/
|
||||
private String buildRedirectUrl(PsyQrcode qrcode)
|
||||
{
|
||||
String qrcodeType = qrcode.getQrcodeType();
|
||||
String targetType = qrcode.getTargetType();
|
||||
Long targetId = qrcode.getTargetId();
|
||||
|
||||
// 根据二维码类型和目标类型构建跳转地址
|
||||
if ("test".equals(qrcodeType))
|
||||
{
|
||||
// 测评类型:跳转到测评开始页面
|
||||
if ("scale".equals(targetType) && targetId != null)
|
||||
{
|
||||
// 跳转到量表测评页面
|
||||
return "/psychology/assessment/start?scaleId=" + targetId;
|
||||
}
|
||||
else if ("questionnaire".equals(targetType) && targetId != null)
|
||||
{
|
||||
// 跳转到问卷开始页面
|
||||
return "/psychology/questionnaire/start?questionnaireId=" + targetId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 跳转到测评选择页面
|
||||
return "/psychology/assessment/start";
|
||||
}
|
||||
}
|
||||
else if ("view_report".equals(qrcodeType))
|
||||
{
|
||||
// 查看报告类型
|
||||
if ("report".equals(targetType) && targetId != null)
|
||||
{
|
||||
// 跳转到报告详情页面
|
||||
return "/psychology/report/detail?reportId=" + targetId;
|
||||
}
|
||||
else if ("assessment".equals(targetType) && targetId != null)
|
||||
{
|
||||
// 通过测评ID查看报告
|
||||
return "/psychology/report/detail?assessmentId=" + targetId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 跳转到报告列表页面
|
||||
return "/psychology/report";
|
||||
}
|
||||
}
|
||||
else if ("register".equals(qrcodeType))
|
||||
{
|
||||
// 注册类型
|
||||
return "/register";
|
||||
}
|
||||
else if ("login".equals(qrcodeType))
|
||||
{
|
||||
// 登录类型
|
||||
return "/login";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认跳转到首页
|
||||
return "/index";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速生成量表测评二维码
|
||||
*
|
||||
* @param scaleId 量表ID
|
||||
* @return 二维码信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:qrcode:add')")
|
||||
@Log(title = "二维码管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/generate/scale")
|
||||
public AjaxResult generateScaleQrcode(@org.springframework.web.bind.annotation.RequestParam Long scaleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
PsyQrcode qrcode = new PsyQrcode();
|
||||
qrcode.setQrcodeType("test");
|
||||
qrcode.setTargetType("scale");
|
||||
qrcode.setTargetId(scaleId);
|
||||
qrcode.setStatus("0");
|
||||
qrcode.setScanCount(0);
|
||||
qrcode.setCreateBy(getUsername());
|
||||
|
||||
int result = qrcodeService.insertQrcode(qrcode);
|
||||
if (result > 0 && qrcode.getQrcodeId() != null)
|
||||
{
|
||||
// 重新查询获取完整信息
|
||||
PsyQrcode savedQrcode = qrcodeService.selectQrcodeById(qrcode.getQrcodeId());
|
||||
if (savedQrcode != null)
|
||||
{
|
||||
// 动态生成Base64二维码图片(不存储到数据库)
|
||||
enrichQrcodeWithBase64(savedQrcode);
|
||||
return success(savedQrcode);
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("二维码生成成功,但查询失败");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("生成二维码失败");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("生成二维码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速生成报告查看二维码
|
||||
*
|
||||
* @param reportId 报告ID
|
||||
* @return 二维码信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:qrcode:add')")
|
||||
@Log(title = "二维码管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/generate/report")
|
||||
public AjaxResult generateReportQrcode(@org.springframework.web.bind.annotation.RequestParam Long reportId)
|
||||
{
|
||||
try
|
||||
{
|
||||
PsyQrcode qrcode = new PsyQrcode();
|
||||
qrcode.setQrcodeType("view_report");
|
||||
qrcode.setTargetType("report");
|
||||
qrcode.setTargetId(reportId);
|
||||
qrcode.setStatus("0");
|
||||
qrcode.setScanCount(0);
|
||||
qrcode.setCreateBy(getUsername());
|
||||
|
||||
int result = qrcodeService.insertQrcode(qrcode);
|
||||
if (result > 0 && qrcode.getQrcodeId() != null)
|
||||
{
|
||||
// 重新查询获取完整信息
|
||||
PsyQrcode savedQrcode = qrcodeService.selectQrcodeById(qrcode.getQrcodeId());
|
||||
if (savedQrcode != null)
|
||||
{
|
||||
// 动态生成Base64二维码图片(不存储到数据库)
|
||||
enrichQrcodeWithBase64(savedQrcode);
|
||||
return success(savedQrcode);
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("二维码生成成功,但查询失败");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("生成二维码失败");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("生成二维码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速生成测评查看报告二维码
|
||||
*
|
||||
* @param assessmentId 测评ID
|
||||
* @return 二维码信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:qrcode:add')")
|
||||
@Log(title = "二维码管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/generate/assessment")
|
||||
public AjaxResult generateAssessmentQrcode(@org.springframework.web.bind.annotation.RequestParam Long assessmentId)
|
||||
{
|
||||
try
|
||||
{
|
||||
PsyQrcode qrcode = new PsyQrcode();
|
||||
qrcode.setQrcodeType("view_report");
|
||||
qrcode.setTargetType("assessment");
|
||||
qrcode.setTargetId(assessmentId);
|
||||
qrcode.setStatus("0");
|
||||
qrcode.setScanCount(0);
|
||||
qrcode.setCreateBy(getUsername());
|
||||
|
||||
int result = qrcodeService.insertQrcode(qrcode);
|
||||
if (result > 0 && qrcode.getQrcodeId() != null)
|
||||
{
|
||||
// 重新查询获取完整信息
|
||||
PsyQrcode savedQrcode = qrcodeService.selectQrcodeById(qrcode.getQrcodeId());
|
||||
if (savedQrcode != null)
|
||||
{
|
||||
// 动态生成Base64二维码图片(不存储到数据库)
|
||||
enrichQrcodeWithBase64(savedQrcode);
|
||||
return success(savedQrcode);
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("二维码生成成功,但查询失败");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return error("生成二维码失败");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("生成二维码失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
package com.ddnai.web.controller.psychology;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.core.page.TableDataInfo;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.common.utils.SecurityUtils;
|
||||
import com.ddnai.common.utils.ServletUtils;
|
||||
import com.ddnai.common.utils.ip.IpUtils;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireItem;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireService;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireItemService;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireAnswerService;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 问卷答题 信息操作处理
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/psychology/questionnaire/answer")
|
||||
public class PsyQuestionnaireAnswerController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPsyQuestionnaireService questionnaireService;
|
||||
|
||||
@Autowired
|
||||
private IPsyQuestionnaireItemService itemService;
|
||||
|
||||
@Autowired
|
||||
private IPsyQuestionnaireAnswerService answerService;
|
||||
|
||||
/**
|
||||
* 获取问卷答题列表
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PsyQuestionnaireAnswer answer)
|
||||
{
|
||||
startPage();
|
||||
List<PsyQuestionnaireAnswer> list = answerService.selectAnswerList(answer);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据答题ID获取详细信息
|
||||
*/
|
||||
@GetMapping(value = "/{answerId}")
|
||||
public AjaxResult getInfo(@PathVariable Long answerId)
|
||||
{
|
||||
return success(answerService.selectAnswerById(answerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始问卷答题
|
||||
*/
|
||||
@PostMapping("/start")
|
||||
public AjaxResult start(@RequestBody Map<String, Object> params)
|
||||
{
|
||||
Long questionnaireId = Long.valueOf(params.get("questionnaireId").toString());
|
||||
String respondentName = params.get("respondentName") != null ? params.get("respondentName").toString() : null;
|
||||
|
||||
PsyQuestionnaireAnswer answer = new PsyQuestionnaireAnswer();
|
||||
answer.setQuestionnaireId(questionnaireId);
|
||||
answer.setUserId(SecurityUtils.getUserId());
|
||||
answer.setRespondentName(respondentName);
|
||||
answer.setStatus("0"); // 进行中
|
||||
answer.setStartTime(new Date());
|
||||
answer.setCreateBy(SecurityUtils.getUsername());
|
||||
|
||||
int result = answerService.insertAnswer(answer);
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
return success(answer.getAnswerId());
|
||||
}
|
||||
return error("开始答题失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷的题目列表
|
||||
*/
|
||||
@GetMapping("/items/{questionnaireId}")
|
||||
public AjaxResult getItems(@PathVariable Long questionnaireId)
|
||||
{
|
||||
List<PsyQuestionnaireItem> items = itemService.selectItemListByQuestionnaireId(questionnaireId);
|
||||
return success(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取答题的答案详情列表
|
||||
*/
|
||||
@GetMapping("/details/{answerId}")
|
||||
public AjaxResult getDetails(@PathVariable Long answerId)
|
||||
{
|
||||
List<PsyQuestionnaireAnswerDetail> details = answerService.selectDetailListByAnswerId(answerId);
|
||||
return success(details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存答案
|
||||
*/
|
||||
@PostMapping("/save")
|
||||
public AjaxResult saveAnswer(@RequestBody Map<String, Object> params)
|
||||
{
|
||||
Long answerId = Long.valueOf(params.get("answerId").toString());
|
||||
Long itemId = Long.valueOf(params.get("itemId").toString());
|
||||
Long optionId = params.get("optionId") != null ? Long.valueOf(params.get("optionId").toString()) : null;
|
||||
String optionIds = params.get("optionIds") != null ? params.get("optionIds").toString() : null;
|
||||
String answerText = params.get("answerText") != null ? params.get("answerText").toString() : null;
|
||||
|
||||
PsyQuestionnaireAnswerDetail detail = new PsyQuestionnaireAnswerDetail();
|
||||
detail.setAnswerId(answerId);
|
||||
detail.setItemId(itemId);
|
||||
detail.setOptionId(optionId);
|
||||
detail.setOptionIds(optionIds);
|
||||
detail.setAnswerText(answerText);
|
||||
detail.setAnswerScore(java.math.BigDecimal.ZERO);
|
||||
detail.setIsSubjective("0");
|
||||
detail.setIsScored("0");
|
||||
|
||||
int result = answerService.saveOrUpdateAnswerDetail(detail);
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
return success("保存成功");
|
||||
}
|
||||
return error("保存失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交问卷
|
||||
*/
|
||||
@PostMapping("/submit/{answerId}")
|
||||
public AjaxResult submit(@PathVariable Long answerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
PsyQuestionnaireAnswer answer = answerService.selectAnswerById(answerId);
|
||||
if (answer == null)
|
||||
{
|
||||
return error("答题记录不存在");
|
||||
}
|
||||
|
||||
if (!"0".equals(answer.getStatus()))
|
||||
{
|
||||
return error("问卷已完成或已作废");
|
||||
}
|
||||
|
||||
// 自动评分并提交
|
||||
int result = answerService.submitAnswer(answerId);
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
return success("提交成功");
|
||||
}
|
||||
return error("提交失败");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("提交失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取问卷成绩排名列表
|
||||
*/
|
||||
@GetMapping("/rank/{questionnaireId}")
|
||||
public AjaxResult getRankList(@PathVariable Long questionnaireId)
|
||||
{
|
||||
PsyQuestionnaireAnswer query = new PsyQuestionnaireAnswer();
|
||||
query.setQuestionnaireId(questionnaireId);
|
||||
query.setStatus("1"); // 只查询已完成的
|
||||
List<PsyQuestionnaireAnswer> list = answerService.selectAnswerList(query);
|
||||
|
||||
// 按总分降序排序,总分相同按提交时间升序
|
||||
list.sort((a, b) -> {
|
||||
if (a.getTotalScore() == null && b.getTotalScore() == null) return 0;
|
||||
if (a.getTotalScore() == null) return 1;
|
||||
if (b.getTotalScore() == null) return -1;
|
||||
int compare = b.getTotalScore().compareTo(a.getTotalScore());
|
||||
if (compare != 0) return compare;
|
||||
// 总分相同,按提交时间排序(先提交的排名靠前)
|
||||
if (a.getSubmitTime() != null && b.getSubmitTime() != null) {
|
||||
return a.getSubmitTime().compareTo(b.getSubmitTime());
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动生成问卷报告(用于测试和修复)
|
||||
*/
|
||||
@PostMapping("/generateReport/{answerId}")
|
||||
public AjaxResult generateReport(@PathVariable Long answerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通过反射调用私有方法,或者将方法改为public
|
||||
// 这里我们直接调用service的公共方法
|
||||
// 由于generateQuestionnaireReport是private,我们需要创建一个公共方法
|
||||
answerService.generateReport(answerId);
|
||||
return success("报告生成成功");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return error("报告生成失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.ddnai.web.controller.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireItem;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireItemService;
|
||||
|
||||
/**
|
||||
* 问卷题目表 信息操作处理
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/psychology/questionnaire/item")
|
||||
public class PsyQuestionnaireItemController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPsyQuestionnaireItemService itemService;
|
||||
|
||||
/**
|
||||
* 查询问卷的所有题目
|
||||
*/
|
||||
@GetMapping("/list/{questionnaireId}")
|
||||
public AjaxResult list(@PathVariable Long questionnaireId)
|
||||
{
|
||||
List<PsyQuestionnaireItem> list = itemService.selectItemListByQuestionnaireId(questionnaireId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据题目ID获取详细信息
|
||||
*/
|
||||
@GetMapping(value = "/{itemId}")
|
||||
public AjaxResult getInfo(@PathVariable Long itemId)
|
||||
{
|
||||
return success(itemService.selectItemById(itemId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增题目
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:add')")
|
||||
@Log(title = "问卷题目", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PsyQuestionnaireItem item)
|
||||
{
|
||||
item.setCreateBy(getUsername());
|
||||
return toAjax(itemService.insertItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改题目
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:edit')")
|
||||
@Log(title = "问卷题目", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody PsyQuestionnaireItem item)
|
||||
{
|
||||
item.setUpdateBy(getUsername());
|
||||
return toAjax(itemService.updateItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:remove')")
|
||||
@Log(title = "问卷题目", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{itemIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] itemIds)
|
||||
{
|
||||
return toAjax(itemService.deleteItemByIds(itemIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.ddnai.web.controller.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireOption;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireOptionService;
|
||||
|
||||
/**
|
||||
* 问卷选项表 信息操作处理
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/psychology/questionnaire/option")
|
||||
public class PsyQuestionnaireOptionController extends BaseController
|
||||
{
|
||||
@Autowired
|
||||
private IPsyQuestionnaireOptionService optionService;
|
||||
|
||||
/**
|
||||
* 查询题目选项列表
|
||||
*/
|
||||
@GetMapping("/list/{itemId}")
|
||||
public AjaxResult listByItemId(@PathVariable Long itemId)
|
||||
{
|
||||
List<PsyQuestionnaireOption> list = optionService.selectOptionListByItemId(itemId);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据选项ID获取详细信息
|
||||
*/
|
||||
@GetMapping(value = "/{optionId}")
|
||||
public AjaxResult getInfo(@PathVariable Long optionId)
|
||||
{
|
||||
return success(optionService.selectOptionById(optionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增选项
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:add')")
|
||||
@Log(title = "问卷选项", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody PsyQuestionnaireOption option)
|
||||
{
|
||||
option.setCreateBy(getUsername());
|
||||
return toAjax(optionService.insertOption(option));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改选项
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:edit')")
|
||||
@Log(title = "问卷选项", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody PsyQuestionnaireOption option)
|
||||
{
|
||||
option.setUpdateBy(getUsername());
|
||||
return toAjax(optionService.updateOption(option));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除选项
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:questionnaire:remove')")
|
||||
@Log(title = "问卷选项", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{optionIds}")
|
||||
public AjaxResult remove(@PathVariable Long[] optionIds)
|
||||
{
|
||||
return toAjax(optionService.deleteOptionByIds(optionIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,15 +14,21 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import com.ddnai.common.annotation.Log;
|
||||
import com.ddnai.common.core.controller.BaseController;
|
||||
import com.ddnai.common.core.domain.AjaxResult;
|
||||
import com.ddnai.common.core.page.TableDataInfo;
|
||||
import com.ddnai.common.enums.BusinessType;
|
||||
import com.ddnai.system.domain.psychology.PsyScale;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaire;
|
||||
import com.ddnai.system.domain.psychology.vo.ScaleImportVO;
|
||||
import com.ddnai.system.service.psychology.IDocumentParseService;
|
||||
import com.ddnai.system.service.psychology.IPsyScaleService;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 心理量表 信息操作处理
|
||||
|
|
@ -38,17 +44,144 @@ public class PsyScaleController extends BaseController
|
|||
|
||||
@Autowired
|
||||
private IDocumentParseService documentParseService;
|
||||
|
||||
@Autowired
|
||||
private IPsyQuestionnaireService questionnaireService;
|
||||
|
||||
/**
|
||||
* 获取量表列表
|
||||
* 获取量表列表(包含问卷)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:scale:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PsyScale scale)
|
||||
public TableDataInfo list(PsyScale scale, @RequestParam(required = false, defaultValue = "true") Boolean includeQuestionnaire)
|
||||
{
|
||||
startPage();
|
||||
List<PsyScale> list = scaleService.selectScaleList(scale);
|
||||
return getDataTable(list);
|
||||
// 如果需要包含问卷,需要先查询所有数据,合并后再分页
|
||||
if (includeQuestionnaire != null && includeQuestionnaire)
|
||||
{
|
||||
// 查询所有量表(不分页)
|
||||
List<PsyScale> scaleList = scaleService.selectScaleList(scale);
|
||||
|
||||
// 为量表数据设置sourceType
|
||||
for (PsyScale s : scaleList)
|
||||
{
|
||||
if (s.getSourceType() == null || s.getSourceType().isEmpty())
|
||||
{
|
||||
s.setSourceType("scale");
|
||||
}
|
||||
}
|
||||
|
||||
// 查询问卷列表(不分页,因为需要合并后再分页)
|
||||
PsyQuestionnaire questionnaireQuery = new PsyQuestionnaire();
|
||||
// 如果量表查询条件中有状态,也应用到问卷查询
|
||||
if (scale.getStatus() != null && !scale.getStatus().isEmpty())
|
||||
{
|
||||
questionnaireQuery.setStatus(scale.getStatus());
|
||||
}
|
||||
// 如果量表查询条件中有名称,也应用到问卷查询
|
||||
if (scale.getScaleName() != null && !scale.getScaleName().isEmpty())
|
||||
{
|
||||
questionnaireQuery.setQuestionnaireName(scale.getScaleName());
|
||||
}
|
||||
// 如果量表查询条件中有编码,也应用到问卷查询
|
||||
if (scale.getScaleCode() != null && !scale.getScaleCode().isEmpty())
|
||||
{
|
||||
questionnaireQuery.setQuestionnaireCode(scale.getScaleCode());
|
||||
}
|
||||
List<PsyQuestionnaire> questionnaireList = questionnaireService.selectQuestionnaireList(questionnaireQuery);
|
||||
|
||||
// 将问卷转换为量表格式
|
||||
for (PsyQuestionnaire questionnaire : questionnaireList)
|
||||
{
|
||||
PsyScale scaleItem = convertQuestionnaireToScale(questionnaire);
|
||||
scaleList.add(scaleItem);
|
||||
}
|
||||
|
||||
// 按排序顺序和创建时间排序
|
||||
scaleList.sort((a, b) -> {
|
||||
int sortCompare = Integer.compare(
|
||||
a.getSortOrder() != null ? a.getSortOrder() : 0,
|
||||
b.getSortOrder() != null ? b.getSortOrder() : 0
|
||||
);
|
||||
if (sortCompare != 0) {
|
||||
return sortCompare;
|
||||
}
|
||||
// 如果排序相同,按创建时间倒序
|
||||
if (a.getCreateTime() != null && b.getCreateTime() != null) {
|
||||
return b.getCreateTime().compareTo(a.getCreateTime());
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 手动分页处理(因为合并后需要重新分页)
|
||||
com.ddnai.common.core.page.PageDomain pageDomain = com.ddnai.common.core.page.TableSupport.buildPageRequest();
|
||||
int pageNum = pageDomain.getPageNum() != null ? pageDomain.getPageNum() : 1;
|
||||
int pageSize = pageDomain.getPageSize() != null ? pageDomain.getPageSize() : 10;
|
||||
int total = scaleList.size();
|
||||
int start = (pageNum - 1) * pageSize;
|
||||
int end = Math.min(start + pageSize, total);
|
||||
|
||||
List<PsyScale> pagedList;
|
||||
if (start < total)
|
||||
{
|
||||
pagedList = scaleList.subList(start, end);
|
||||
}
|
||||
else
|
||||
{
|
||||
pagedList = new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
// 创建分页结果
|
||||
TableDataInfo dataTable = new TableDataInfo();
|
||||
dataTable.setCode(200);
|
||||
dataTable.setMsg("查询成功");
|
||||
dataTable.setRows(pagedList);
|
||||
dataTable.setTotal(total);
|
||||
return dataTable;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 只查询量表,使用正常分页
|
||||
startPage();
|
||||
List<PsyScale> scaleList = scaleService.selectScaleList(scale);
|
||||
|
||||
// 为量表数据设置sourceType
|
||||
for (PsyScale s : scaleList)
|
||||
{
|
||||
if (s.getSourceType() == null || s.getSourceType().isEmpty())
|
||||
{
|
||||
s.setSourceType("scale");
|
||||
}
|
||||
}
|
||||
|
||||
return getDataTable(scaleList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将问卷转换为量表格式
|
||||
*/
|
||||
private PsyScale convertQuestionnaireToScale(PsyQuestionnaire questionnaire)
|
||||
{
|
||||
PsyScale scale = new PsyScale();
|
||||
// 使用问卷ID作为scaleId,但添加前缀标识(使用负数或特殊值)
|
||||
// 为了区分,我们使用负数:-questionnaireId
|
||||
scale.setScaleId(-questionnaire.getQuestionnaireId());
|
||||
scale.setOriginalId(questionnaire.getQuestionnaireId());
|
||||
scale.setSourceType("questionnaire");
|
||||
scale.setScaleCode(questionnaire.getQuestionnaireCode());
|
||||
scale.setScaleName(questionnaire.getQuestionnaireName());
|
||||
// 问卷没有英文名,使用问卷类型作为scaleType
|
||||
scale.setScaleType(questionnaire.getQuestionnaireType());
|
||||
scale.setItemCount(questionnaire.getItemCount());
|
||||
scale.setEstimatedTime(questionnaire.getEstimatedTime());
|
||||
scale.setStatus(questionnaire.getStatus());
|
||||
scale.setSortOrder(questionnaire.getSortOrder());
|
||||
scale.setCreateBy(questionnaire.getCreateBy());
|
||||
scale.setCreateTime(questionnaire.getCreateTime());
|
||||
scale.setUpdateBy(questionnaire.getUpdateBy());
|
||||
scale.setUpdateTime(questionnaire.getUpdateTime());
|
||||
scale.setRemark(questionnaire.getRemark());
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -253,5 +386,102 @@ public class PsyScaleController extends BaseController
|
|||
return error("提取文本失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出量表(JSON格式)
|
||||
* 支持单个或批量导出
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('psychology:scale:export')")
|
||||
@Log(title = "心理量表", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void exportScales(@RequestParam(value = "scaleIds", required = false) String scaleIdsStr, HttpServletResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 如果没有指定scaleIds,导出所有量表
|
||||
List<ScaleImportVO> exportList;
|
||||
if (scaleIdsStr == null || scaleIdsStr.trim().isEmpty())
|
||||
{
|
||||
// 获取所有量表
|
||||
List<PsyScale> allScales = scaleService.selectScaleList(new PsyScale());
|
||||
Long[] allScaleIds = allScales.stream().map(PsyScale::getScaleId).toArray(Long[]::new);
|
||||
exportList = scaleService.exportScales(allScaleIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 解析scaleIds字符串(逗号分隔)
|
||||
String[] scaleIdStrs = scaleIdsStr.split(",");
|
||||
Long[] scaleIds = new Long[scaleIdStrs.length];
|
||||
for (int i = 0; i < scaleIdStrs.length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
scaleIds[i] = Long.parseLong(scaleIdStrs[i].trim());
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"error\":\"无效的量表ID: " + scaleIdStrs[i] + "\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
exportList = scaleService.exportScales(scaleIds);
|
||||
}
|
||||
|
||||
if (exportList == null || exportList.isEmpty())
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"error\":\"没有可导出的量表数据\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
String filename;
|
||||
if (exportList.size() == 1)
|
||||
{
|
||||
ScaleImportVO singleScale = exportList.get(0);
|
||||
String scaleName = singleScale.getScale() != null ? singleScale.getScale().getScaleName() : "量表";
|
||||
filename = scaleName + "_" + System.currentTimeMillis() + ".json";
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = "量表批量导出_" + System.currentTimeMillis() + ".json";
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" +
|
||||
URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()) + "\"");
|
||||
|
||||
// 使用Jackson序列化为JSON
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT, true);
|
||||
|
||||
// 如果只有一个量表,直接导出该量表对象;如果有多个,导出为数组
|
||||
if (exportList.size() == 1)
|
||||
{
|
||||
objectMapper.writeValue(response.getOutputStream(), exportList.get(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
objectMapper.writeValue(response.getOutputStream(), exportList);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("{\"error\":\"导出失败:" + e.getMessage().replace("\"", "\\\"") + "\"}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略写入错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ spring:
|
|||
master:
|
||||
url: jdbc:mysql://localhost:3306/ry_news?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
password: root
|
||||
# 从库数据源
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ import org.springframework.security.authentication.ProviderManager;
|
|||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.StrictHttpFirewall;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import com.ddnai.framework.config.properties.PermitAllUrlProperties;
|
||||
import com.ddnai.framework.security.filter.JwtAuthenticationTokenFilter;
|
||||
|
|
@ -78,6 +81,27 @@ public class SecurityConfig
|
|||
return new ProviderManager(daoAuthenticationProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置HttpFirewall允许URL中包含分号
|
||||
* 解决RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
|
||||
*/
|
||||
@Bean
|
||||
public HttpFirewall httpFirewall()
|
||||
{
|
||||
StrictHttpFirewall firewall = new StrictHttpFirewall();
|
||||
firewall.setAllowSemicolon(true);
|
||||
return firewall;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置WebSecurity使用自定义的HttpFirewall
|
||||
*/
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer(HttpFirewall httpFirewall)
|
||||
{
|
||||
return (web) -> web.httpFirewall(httpFirewall);
|
||||
}
|
||||
|
||||
/**
|
||||
* anyRequest | 匹配所有请求路径
|
||||
* access | SpringEl表达式结果为true时可以访问
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import javax.validation.constraints.NotBlank;
|
|||
import javax.validation.constraints.Size;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ddnai.common.annotation.Excel;
|
||||
import com.ddnai.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ public class PsyQrcode extends BaseEntity
|
|||
private Long qrcodeId;
|
||||
|
||||
/** 二维码编号 */
|
||||
@NotBlank(message = "二维码编号不能为空")
|
||||
@Size(min = 0, max = 50, message = "二维码编号不能超过50个字符")
|
||||
private String qrcodeCode;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
package com.ddnai.system.domain.psychology;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
/**
|
||||
* 问卷答案详情表 psy_questionnaire_answer_detail
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public class PsyQuestionnaireAnswerDetail
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 详情ID */
|
||||
private Long detailId;
|
||||
|
||||
/** 答案ID(关联问卷答题记录) */
|
||||
private Long answerId;
|
||||
|
||||
/** 题目ID */
|
||||
private Long itemId;
|
||||
|
||||
/** 选项ID(单选/判断时使用) */
|
||||
private Long optionId;
|
||||
|
||||
/** 选项ID列表(多选时使用,逗号分隔) */
|
||||
private String optionIds;
|
||||
|
||||
/** 文本答案(填空、简答、问答、作文等) */
|
||||
private String answerText;
|
||||
|
||||
/** 答案得分 */
|
||||
private BigDecimal answerScore;
|
||||
|
||||
/** 是否主观题(0否 1是) */
|
||||
private String isSubjective;
|
||||
|
||||
/** 是否已评分(0否 1是) */
|
||||
private String isScored;
|
||||
|
||||
/** 评分人 */
|
||||
private String scoredBy;
|
||||
|
||||
/** 评分时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private java.util.Date scoredTime;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private java.util.Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private java.util.Date updateTime;
|
||||
|
||||
public Long getDetailId()
|
||||
{
|
||||
return detailId;
|
||||
}
|
||||
|
||||
public void setDetailId(Long detailId)
|
||||
{
|
||||
this.detailId = detailId;
|
||||
}
|
||||
|
||||
public Long getAnswerId()
|
||||
{
|
||||
return answerId;
|
||||
}
|
||||
|
||||
public void setAnswerId(Long answerId)
|
||||
{
|
||||
this.answerId = answerId;
|
||||
}
|
||||
|
||||
public Long getItemId()
|
||||
{
|
||||
return itemId;
|
||||
}
|
||||
|
||||
public void setItemId(Long itemId)
|
||||
{
|
||||
this.itemId = itemId;
|
||||
}
|
||||
|
||||
public Long getOptionId()
|
||||
{
|
||||
return optionId;
|
||||
}
|
||||
|
||||
public void setOptionId(Long optionId)
|
||||
{
|
||||
this.optionId = optionId;
|
||||
}
|
||||
|
||||
public String getOptionIds()
|
||||
{
|
||||
return optionIds;
|
||||
}
|
||||
|
||||
public void setOptionIds(String optionIds)
|
||||
{
|
||||
this.optionIds = optionIds;
|
||||
}
|
||||
|
||||
public String getAnswerText()
|
||||
{
|
||||
return answerText;
|
||||
}
|
||||
|
||||
public void setAnswerText(String answerText)
|
||||
{
|
||||
this.answerText = answerText;
|
||||
}
|
||||
|
||||
public BigDecimal getAnswerScore()
|
||||
{
|
||||
return answerScore;
|
||||
}
|
||||
|
||||
public void setAnswerScore(BigDecimal answerScore)
|
||||
{
|
||||
this.answerScore = answerScore;
|
||||
}
|
||||
|
||||
public String getIsSubjective()
|
||||
{
|
||||
return isSubjective;
|
||||
}
|
||||
|
||||
public void setIsSubjective(String isSubjective)
|
||||
{
|
||||
this.isSubjective = isSubjective;
|
||||
}
|
||||
|
||||
public String getIsScored()
|
||||
{
|
||||
return isScored;
|
||||
}
|
||||
|
||||
public void setIsScored(String isScored)
|
||||
{
|
||||
this.isScored = isScored;
|
||||
}
|
||||
|
||||
public String getScoredBy()
|
||||
{
|
||||
return scoredBy;
|
||||
}
|
||||
|
||||
public void setScoredBy(String scoredBy)
|
||||
{
|
||||
this.scoredBy = scoredBy;
|
||||
}
|
||||
|
||||
public java.util.Date getScoredTime()
|
||||
{
|
||||
return scoredTime;
|
||||
}
|
||||
|
||||
public void setScoredTime(java.util.Date scoredTime)
|
||||
{
|
||||
this.scoredTime = scoredTime;
|
||||
}
|
||||
|
||||
public java.util.Date getCreateTime()
|
||||
{
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(java.util.Date createTime)
|
||||
{
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public java.util.Date getUpdateTime()
|
||||
{
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(java.util.Date updateTime)
|
||||
{
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("detailId", getDetailId())
|
||||
.append("answerId", getAnswerId())
|
||||
.append("itemId", getItemId())
|
||||
.append("optionId", getOptionId())
|
||||
.append("optionIds", getOptionIds())
|
||||
.append("answerText", getAnswerText())
|
||||
.append("answerScore", getAnswerScore())
|
||||
.append("isSubjective", getIsSubjective())
|
||||
.append("isScored", getIsScored())
|
||||
.append("scoredBy", getScoredBy())
|
||||
.append("scoredTime", getScoredTime())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.ddnai.system.domain.psychology;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.ddnai.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 问卷选项表 psy_questionnaire_option
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public class PsyQuestionnaireOption extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 选项ID */
|
||||
private Long optionId;
|
||||
|
||||
/** 题目ID */
|
||||
private Long itemId;
|
||||
|
||||
/** 选项编码(如A、B、C或1、2、3) */
|
||||
private String optionCode;
|
||||
|
||||
/** 选项内容 */
|
||||
private String optionContent;
|
||||
|
||||
/** 选项分值 */
|
||||
private BigDecimal optionScore;
|
||||
|
||||
/** 是否正确答案(0否 1是) */
|
||||
private String isCorrect;
|
||||
|
||||
/** 排序顺序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
public Long getOptionId()
|
||||
{
|
||||
return optionId;
|
||||
}
|
||||
|
||||
public void setOptionId(Long optionId)
|
||||
{
|
||||
this.optionId = optionId;
|
||||
}
|
||||
|
||||
public Long getItemId()
|
||||
{
|
||||
return itemId;
|
||||
}
|
||||
|
||||
public void setItemId(Long itemId)
|
||||
{
|
||||
this.itemId = itemId;
|
||||
}
|
||||
|
||||
public String getOptionCode()
|
||||
{
|
||||
return optionCode;
|
||||
}
|
||||
|
||||
public void setOptionCode(String optionCode)
|
||||
{
|
||||
this.optionCode = optionCode;
|
||||
}
|
||||
|
||||
public String getOptionContent()
|
||||
{
|
||||
return optionContent;
|
||||
}
|
||||
|
||||
public void setOptionContent(String optionContent)
|
||||
{
|
||||
this.optionContent = optionContent;
|
||||
}
|
||||
|
||||
public BigDecimal getOptionScore()
|
||||
{
|
||||
return optionScore;
|
||||
}
|
||||
|
||||
public void setOptionScore(BigDecimal optionScore)
|
||||
{
|
||||
this.optionScore = optionScore;
|
||||
}
|
||||
|
||||
public String getIsCorrect()
|
||||
{
|
||||
return isCorrect;
|
||||
}
|
||||
|
||||
public void setIsCorrect(String isCorrect)
|
||||
{
|
||||
this.isCorrect = isCorrect;
|
||||
}
|
||||
|
||||
public Integer getSortOrder()
|
||||
{
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public void setSortOrder(Integer sortOrder)
|
||||
{
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("optionId", getOptionId())
|
||||
.append("itemId", getItemId())
|
||||
.append("optionCode", getOptionCode())
|
||||
.append("optionContent", getOptionContent())
|
||||
.append("optionScore", getOptionScore())
|
||||
.append("isCorrect", getIsCorrect())
|
||||
.append("sortOrder", getSortOrder())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.ddnai.system.domain.psychology;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ddnai.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 问卷报告表 psy_questionnaire_report
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public class PsyQuestionnaireReport extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 报告ID */
|
||||
private Long reportId;
|
||||
|
||||
/** 答题ID(关联问卷答题记录) */
|
||||
@NotNull(message = "答题ID不能为空")
|
||||
private Long answerId;
|
||||
|
||||
/** 报告类型(standard标准 detailed详细 brief简要) */
|
||||
private String reportType;
|
||||
|
||||
/** 报告标题 */
|
||||
private String reportTitle;
|
||||
|
||||
/** 报告内容(HTML格式) */
|
||||
private String reportContent;
|
||||
|
||||
/** 报告摘要 */
|
||||
private String summary;
|
||||
|
||||
/** 图表数据(JSON格式) */
|
||||
private String chartData;
|
||||
|
||||
/** PDF文件路径 */
|
||||
private String pdfPath;
|
||||
|
||||
/** 是否已生成(0否 1是) */
|
||||
private String isGenerated;
|
||||
|
||||
/** 生成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private java.util.Date generateTime;
|
||||
|
||||
public Long getReportId()
|
||||
{
|
||||
return reportId;
|
||||
}
|
||||
|
||||
public void setReportId(Long reportId)
|
||||
{
|
||||
this.reportId = reportId;
|
||||
}
|
||||
|
||||
public Long getAnswerId()
|
||||
{
|
||||
return answerId;
|
||||
}
|
||||
|
||||
public void setAnswerId(Long answerId)
|
||||
{
|
||||
this.answerId = answerId;
|
||||
}
|
||||
|
||||
public String getReportType()
|
||||
{
|
||||
return reportType;
|
||||
}
|
||||
|
||||
public void setReportType(String reportType)
|
||||
{
|
||||
this.reportType = reportType;
|
||||
}
|
||||
|
||||
public String getReportTitle()
|
||||
{
|
||||
return reportTitle;
|
||||
}
|
||||
|
||||
public void setReportTitle(String reportTitle)
|
||||
{
|
||||
this.reportTitle = reportTitle;
|
||||
}
|
||||
|
||||
public String getReportContent()
|
||||
{
|
||||
return reportContent;
|
||||
}
|
||||
|
||||
public void setReportContent(String reportContent)
|
||||
{
|
||||
this.reportContent = reportContent;
|
||||
}
|
||||
|
||||
public String getSummary()
|
||||
{
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary)
|
||||
{
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getChartData()
|
||||
{
|
||||
return chartData;
|
||||
}
|
||||
|
||||
public void setChartData(String chartData)
|
||||
{
|
||||
this.chartData = chartData;
|
||||
}
|
||||
|
||||
public String getPdfPath()
|
||||
{
|
||||
return pdfPath;
|
||||
}
|
||||
|
||||
public void setPdfPath(String pdfPath)
|
||||
{
|
||||
this.pdfPath = pdfPath;
|
||||
}
|
||||
|
||||
public String getIsGenerated()
|
||||
{
|
||||
return isGenerated;
|
||||
}
|
||||
|
||||
public void setIsGenerated(String isGenerated)
|
||||
{
|
||||
this.isGenerated = isGenerated;
|
||||
}
|
||||
|
||||
public java.util.Date getGenerateTime()
|
||||
{
|
||||
return generateTime;
|
||||
}
|
||||
|
||||
public void setGenerateTime(java.util.Date generateTime)
|
||||
{
|
||||
this.generateTime = generateTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
|
||||
.append("reportId", getReportId())
|
||||
.append("answerId", getAnswerId())
|
||||
.append("reportType", getReportType())
|
||||
.append("reportTitle", getReportTitle())
|
||||
.append("reportContent", getReportContent())
|
||||
.append("summary", getSummary())
|
||||
.append("chartData", getChartData())
|
||||
.append("pdfPath", getPdfPath())
|
||||
.append("isGenerated", getIsGenerated())
|
||||
.append("generateTime", getGenerateTime())
|
||||
.append("createBy", getCreateBy())
|
||||
.append("createTime", getCreateTime())
|
||||
.append("updateBy", getUpdateBy())
|
||||
.append("updateTime", getUpdateTime())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +68,12 @@ public class PsyScale extends BaseEntity
|
|||
/** 排序顺序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 来源类型(scale量表 questionnaire问卷) */
|
||||
private String sourceType;
|
||||
|
||||
/** 原始ID(如果是问卷,存储questionnaireId) */
|
||||
private Long originalId;
|
||||
|
||||
public Long getScaleId()
|
||||
{
|
||||
return scaleId;
|
||||
|
|
@ -228,6 +234,26 @@ public class PsyScale extends BaseEntity
|
|||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public String getSourceType()
|
||||
{
|
||||
return sourceType;
|
||||
}
|
||||
|
||||
public void setSourceType(String sourceType)
|
||||
{
|
||||
this.sourceType = sourceType;
|
||||
}
|
||||
|
||||
public Long getOriginalId()
|
||||
{
|
||||
return originalId;
|
||||
}
|
||||
|
||||
public void setOriginalId(Long originalId)
|
||||
{
|
||||
this.originalId = originalId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.ddnai.system.domain.psychology.vo;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 解释配置导入VO(支持factorCode映射)
|
||||
* 解释配置导入VO(支持factorCode映射和scoreMin/scoreMax字段)
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
|
|
@ -18,6 +19,24 @@ public class InterpretationImportVO extends PsyResultInterpretation
|
|||
@JsonProperty("factorCode")
|
||||
private String factorCode;
|
||||
|
||||
/**
|
||||
* 支持导入JSON中的scoreMin字段(映射到scoreRangeMin)
|
||||
*/
|
||||
@JsonProperty("scoreMin")
|
||||
public void setScoreMin(BigDecimal scoreMin)
|
||||
{
|
||||
this.setScoreRangeMin(scoreMin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持导入JSON中的scoreMax字段(映射到scoreRangeMax)
|
||||
*/
|
||||
@JsonProperty("scoreMax")
|
||||
public void setScoreMax(BigDecimal scoreMax)
|
||||
{
|
||||
this.setScoreRangeMax(scoreMax);
|
||||
}
|
||||
|
||||
public String getFactorCode()
|
||||
{
|
||||
return factorCode;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
package com.ddnai.system.domain.psychology.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import com.ddnai.common.annotation.Excel;
|
||||
import com.ddnai.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 报告导出VO
|
||||
* 用于Excel导出报告数据
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public class ReportExportVO extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 报告ID */
|
||||
@Excel(name = "报告ID", sort = 1)
|
||||
private Long reportId;
|
||||
|
||||
/** 测评ID */
|
||||
@Excel(name = "测评ID", sort = 2)
|
||||
private Long assessmentId;
|
||||
|
||||
/** 报告标题 */
|
||||
@Excel(name = "报告标题", sort = 3, width = 30)
|
||||
private String reportTitle;
|
||||
|
||||
/** 报告类型 */
|
||||
@Excel(name = "报告类型", sort = 4, readConverterExp = "standard=标准报告,detailed=详细报告,brief=简要报告")
|
||||
private String reportType;
|
||||
|
||||
/** 报告摘要 */
|
||||
@Excel(name = "报告摘要", sort = 5, width = 50, height = 5)
|
||||
private String summary;
|
||||
|
||||
/** 报告内容(纯文本,去除HTML标签) */
|
||||
@Excel(name = "报告内容", sort = 6, width = 80, height = 10)
|
||||
private String reportContentText;
|
||||
|
||||
/** 是否已生成 */
|
||||
@Excel(name = "生成状态", sort = 7, readConverterExp = "0=未生成,1=已生成")
|
||||
private String isGenerated;
|
||||
|
||||
/** 生成时间 */
|
||||
@Excel(name = "生成时间", sort = 8, width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date generateTime;
|
||||
|
||||
/** 创建时间 */
|
||||
@Excel(name = "创建时间", sort = 9, width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
public Long getReportId()
|
||||
{
|
||||
return reportId;
|
||||
}
|
||||
|
||||
public void setReportId(Long reportId)
|
||||
{
|
||||
this.reportId = reportId;
|
||||
}
|
||||
|
||||
public Long getAssessmentId()
|
||||
{
|
||||
return assessmentId;
|
||||
}
|
||||
|
||||
public void setAssessmentId(Long assessmentId)
|
||||
{
|
||||
this.assessmentId = assessmentId;
|
||||
}
|
||||
|
||||
public String getReportTitle()
|
||||
{
|
||||
return reportTitle;
|
||||
}
|
||||
|
||||
public void setReportTitle(String reportTitle)
|
||||
{
|
||||
this.reportTitle = reportTitle;
|
||||
}
|
||||
|
||||
public String getReportType()
|
||||
{
|
||||
return reportType;
|
||||
}
|
||||
|
||||
public void setReportType(String reportType)
|
||||
{
|
||||
this.reportType = reportType;
|
||||
}
|
||||
|
||||
public String getSummary()
|
||||
{
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary)
|
||||
{
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getReportContentText()
|
||||
{
|
||||
return reportContentText;
|
||||
}
|
||||
|
||||
public void setReportContentText(String reportContentText)
|
||||
{
|
||||
this.reportContentText = reportContentText;
|
||||
}
|
||||
|
||||
public String getIsGenerated()
|
||||
{
|
||||
return isGenerated;
|
||||
}
|
||||
|
||||
public void setIsGenerated(String isGenerated)
|
||||
{
|
||||
this.isGenerated = isGenerated;
|
||||
}
|
||||
|
||||
public Date getGenerateTime()
|
||||
{
|
||||
return generateTime;
|
||||
}
|
||||
|
||||
public void setGenerateTime(Date generateTime)
|
||||
{
|
||||
this.generateTime = generateTime;
|
||||
}
|
||||
|
||||
public Date getCreateTime()
|
||||
{
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(Date createTime)
|
||||
{
|
||||
this.createTime = createTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,7 +6,6 @@ import com.ddnai.system.domain.psychology.PsyFactor;
|
|||
import com.ddnai.system.domain.psychology.PsyScaleItem;
|
||||
import com.ddnai.system.domain.psychology.PsyScaleOption;
|
||||
import com.ddnai.system.domain.psychology.PsyFactorRule;
|
||||
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
|
||||
import com.ddnai.system.domain.psychology.PsyWarningRule;
|
||||
|
||||
/**
|
||||
|
|
@ -28,11 +27,11 @@ public class ScaleImportVO
|
|||
|
||||
/** 解释配置列表(可选) */
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.AS_EMPTY)
|
||||
private List<PsyResultInterpretation> interpretations;
|
||||
private List<InterpretationImportVO> interpretations;
|
||||
|
||||
/** 预警规则列表(可选) */
|
||||
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.AS_EMPTY)
|
||||
private List<PsyWarningRule> warningRules;
|
||||
private List<? extends PsyWarningRule> warningRules;
|
||||
|
||||
public PsyScale getScale()
|
||||
{
|
||||
|
|
@ -64,22 +63,23 @@ public class ScaleImportVO
|
|||
this.items = items;
|
||||
}
|
||||
|
||||
public List<PsyResultInterpretation> getInterpretations()
|
||||
public List<InterpretationImportVO> getInterpretations()
|
||||
{
|
||||
return interpretations;
|
||||
}
|
||||
|
||||
public void setInterpretations(List<PsyResultInterpretation> interpretations)
|
||||
public void setInterpretations(List<InterpretationImportVO> interpretations)
|
||||
{
|
||||
this.interpretations = interpretations;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<PsyWarningRule> getWarningRules()
|
||||
{
|
||||
return warningRules;
|
||||
return (List<PsyWarningRule>) warningRules;
|
||||
}
|
||||
|
||||
public void setWarningRules(List<PsyWarningRule> warningRules)
|
||||
public void setWarningRules(List<? extends PsyWarningRule> warningRules)
|
||||
{
|
||||
this.warningRules = warningRules;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,5 +81,14 @@ public interface PsyQrcodeMapper
|
|||
* @return 结果
|
||||
*/
|
||||
public int checkQrcodeCodeUnique(String qrcodeCode);
|
||||
|
||||
/**
|
||||
* 检查二维码编码是否唯一(排除指定ID,用于编辑时)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @param qrcodeId 要排除的二维码ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int checkQrcodeCodeUniqueExcludeSelf(String qrcodeCode, Long qrcodeId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package com.ddnai.system.mapper.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail;
|
||||
|
||||
/**
|
||||
* 问卷答案详情表 数据层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface PsyQuestionnaireAnswerDetailMapper
|
||||
{
|
||||
/**
|
||||
* 查询答案详情信息
|
||||
*
|
||||
* @param detailId 详情ID
|
||||
* @return 答案详情信息
|
||||
*/
|
||||
public PsyQuestionnaireAnswerDetail selectDetailById(Long detailId);
|
||||
|
||||
/**
|
||||
* 查询答案详情列表
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答案详情集合
|
||||
*/
|
||||
public List<PsyQuestionnaireAnswerDetail> selectDetailListByAnswerId(Long answerId);
|
||||
|
||||
/**
|
||||
* 查询答案详情(根据答题ID和题目ID)
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @param itemId 题目ID
|
||||
* @return 答案详情
|
||||
*/
|
||||
public PsyQuestionnaireAnswerDetail selectDetailByAnswerIdAndItemId(@Param("answerId") Long answerId, @Param("itemId") Long itemId);
|
||||
|
||||
/**
|
||||
* 新增答案详情
|
||||
*
|
||||
* @param detail 答案详情信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertDetail(PsyQuestionnaireAnswerDetail detail);
|
||||
|
||||
/**
|
||||
* 修改答案详情
|
||||
*
|
||||
* @param detail 答案详情信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateDetail(PsyQuestionnaireAnswerDetail detail);
|
||||
|
||||
/**
|
||||
* 删除答案详情
|
||||
*
|
||||
* @param detailId 详情ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDetailById(Long detailId);
|
||||
|
||||
/**
|
||||
* 批量删除答案详情
|
||||
*
|
||||
* @param detailIds 需要删除的详情ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteDetailByIds(Long[] detailIds);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.ddnai.system.mapper.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer;
|
||||
|
||||
/**
|
||||
* 问卷答题记录表 数据层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface PsyQuestionnaireAnswerMapper
|
||||
{
|
||||
/**
|
||||
* 查询答题记录信息
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答题记录信息
|
||||
*/
|
||||
public PsyQuestionnaireAnswer selectAnswerById(Long answerId);
|
||||
|
||||
/**
|
||||
* 查询答题记录列表
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 答题记录集合
|
||||
*/
|
||||
public List<PsyQuestionnaireAnswer> selectAnswerList(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 新增答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertAnswer(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 修改答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateAnswer(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 批量删除答题记录
|
||||
*
|
||||
* @param answerIds 需要删除的答题ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteAnswerByIds(Long[] answerIds);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.ddnai.system.mapper.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireOption;
|
||||
|
||||
/**
|
||||
* 问卷选项表 数据层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface PsyQuestionnaireOptionMapper
|
||||
{
|
||||
/**
|
||||
* 查询选项信息
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 选项信息
|
||||
*/
|
||||
public PsyQuestionnaireOption selectOptionById(Long optionId);
|
||||
|
||||
/**
|
||||
* 查询题目选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 选项集合
|
||||
*/
|
||||
public List<PsyQuestionnaireOption> selectOptionListByItemId(Long itemId);
|
||||
|
||||
/**
|
||||
* 查询正确答案选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 正确答案选项集合
|
||||
*/
|
||||
public List<PsyQuestionnaireOption> selectCorrectOptionListByItemId(Long itemId);
|
||||
|
||||
/**
|
||||
* 新增选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertOption(PsyQuestionnaireOption option);
|
||||
|
||||
/**
|
||||
* 修改选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateOption(PsyQuestionnaireOption option);
|
||||
|
||||
/**
|
||||
* 删除选项
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOptionById(Long optionId);
|
||||
|
||||
/**
|
||||
* 批量删除选项
|
||||
*
|
||||
* @param optionIds 需要删除的选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOptionByIds(Long[] optionIds);
|
||||
|
||||
/**
|
||||
* 删除题目的全部选项
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOptionByItemId(Long itemId);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.ddnai.system.mapper.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireReport;
|
||||
|
||||
/**
|
||||
* 问卷报告表 数据层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface PsyQuestionnaireReportMapper
|
||||
{
|
||||
/**
|
||||
* 查询问卷报告信息
|
||||
*
|
||||
* @param reportId 报告ID
|
||||
* @return 报告信息
|
||||
*/
|
||||
public PsyQuestionnaireReport selectReportById(Long reportId);
|
||||
|
||||
/**
|
||||
* 根据答题ID查询报告
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 报告信息
|
||||
*/
|
||||
public PsyQuestionnaireReport selectReportByAnswerId(Long answerId);
|
||||
|
||||
/**
|
||||
* 查询问卷报告列表
|
||||
*
|
||||
* @param report 问卷报告信息
|
||||
* @return 问卷报告集合
|
||||
*/
|
||||
public List<PsyQuestionnaireReport> selectReportList(PsyQuestionnaireReport report);
|
||||
|
||||
/**
|
||||
* 新增问卷报告
|
||||
*
|
||||
* @param report 问卷报告信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertReport(PsyQuestionnaireReport report);
|
||||
|
||||
/**
|
||||
* 修改问卷报告
|
||||
*
|
||||
* @param report 问卷报告信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateReport(PsyQuestionnaireReport report);
|
||||
|
||||
/**
|
||||
* 删除问卷报告
|
||||
*
|
||||
* @param reportId 报告ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteReportById(Long reportId);
|
||||
|
||||
/**
|
||||
* 批量删除问卷报告
|
||||
*
|
||||
* @param reportIds 需要删除的报告ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteReportByIds(Long[] reportIds);
|
||||
}
|
||||
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
package com.ddnai.system.service.impl.psychology;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ddnai.common.config.RuoYiConfig;
|
||||
import com.ddnai.common.utils.DateUtils;
|
||||
import com.ddnai.common.utils.QrCodeUtils;
|
||||
import com.ddnai.common.utils.StringUtils;
|
||||
import com.ddnai.common.utils.file.FileUploadUtils;
|
||||
import com.ddnai.common.utils.uuid.IdUtils;
|
||||
import com.ddnai.system.domain.psychology.PsyQrcode;
|
||||
import com.ddnai.system.mapper.psychology.PsyQrcodeMapper;
|
||||
|
|
@ -35,6 +32,9 @@ public class PsyQrcodeServiceImpl implements IPsyQrcodeService
|
|||
@Value("${server.servlet.context-path:}")
|
||||
private String contextPath;
|
||||
|
||||
@Value("${server.address:localhost}")
|
||||
private String serverAddress;
|
||||
|
||||
/**
|
||||
* 查询二维码信息
|
||||
*
|
||||
|
|
@ -47,6 +47,39 @@ public class PsyQrcodeServiceImpl implements IPsyQrcodeService
|
|||
return qrcodeMapper.selectQrcodeById(qrcodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据编码查询二维码信息
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @return 二维码信息
|
||||
*/
|
||||
@Override
|
||||
public PsyQrcode selectQrcodeByCode(String qrcodeCode)
|
||||
{
|
||||
return qrcodeMapper.selectQrcodeByCode(qrcodeCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描二维码(增加扫码次数)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @return 二维码信息
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public PsyQrcode scanQrcode(String qrcodeCode)
|
||||
{
|
||||
PsyQrcode qrcode = qrcodeMapper.selectQrcodeByCode(qrcodeCode);
|
||||
if (qrcode != null)
|
||||
{
|
||||
// 增加扫码次数
|
||||
qrcodeMapper.incrementScanCount(qrcode.getQrcodeId());
|
||||
// 重新查询获取最新的扫码次数
|
||||
qrcode = qrcodeMapper.selectQrcodeById(qrcode.getQrcodeId());
|
||||
}
|
||||
return qrcode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询二维码列表
|
||||
*
|
||||
|
|
@ -71,33 +104,63 @@ public class PsyQrcodeServiceImpl implements IPsyQrcodeService
|
|||
{
|
||||
qrcode.setCreateTime(DateUtils.getNowDate());
|
||||
|
||||
// 生成唯一编码
|
||||
// 生成唯一编码(如果为空或已存在,则生成新的)
|
||||
// 注意:删除的二维码编码会自动释放,可以重新使用
|
||||
if (StringUtils.isEmpty(qrcode.getQrcodeCode()))
|
||||
{
|
||||
qrcode.setQrcodeCode(IdUtils.fastSimpleUUID());
|
||||
// 为空则自动生成唯一编码
|
||||
qrcode.setQrcodeCode(generateUniqueQrcodeCode());
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果用户提供了编码,检查是否唯一(只检查未删除的记录)
|
||||
// 如果已存在,则自动生成新的唯一编码
|
||||
if (!checkQrcodeCodeUnique(qrcode.getQrcodeCode()))
|
||||
{
|
||||
// 如果编码已被使用,生成新的唯一编码
|
||||
qrcode.setQrcodeCode(generateUniqueQrcodeCode());
|
||||
}
|
||||
// 如果编码唯一(可能是已删除的编码,现在可以重新使用),直接使用
|
||||
}
|
||||
|
||||
// 生成二维码图片
|
||||
String qrcodeBase64 = generateQrcode(qrcode);
|
||||
if (StringUtils.isNotEmpty(qrcodeBase64))
|
||||
{
|
||||
// 保存二维码图片到服务器
|
||||
try
|
||||
{
|
||||
byte[] imageBytes = java.util.Base64.getDecoder().decode(qrcodeBase64);
|
||||
String qrcodePath = com.ddnai.common.utils.file.FileUtils.writeBytes(imageBytes, RuoYiConfig.getUploadPath());
|
||||
qrcode.setQrcodeUrl(qrcodePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// 如果保存失败,使用Base64
|
||||
qrcode.setQrcodeUrl("data:image/png;base64," + qrcodeBase64);
|
||||
}
|
||||
}
|
||||
// 不存储Base64数据到数据库(因为数据太长)
|
||||
// Base64数据将在Controller层动态生成并返回
|
||||
// 这里只存储一个标识符,表示二维码已生成
|
||||
qrcode.setQrcodeUrl("generated");
|
||||
|
||||
return qrcodeMapper.insertQrcode(qrcode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的二维码编码
|
||||
* 删除的二维码编码会自动释放,可以被重新使用
|
||||
*
|
||||
* @return 唯一的二维码编码
|
||||
*/
|
||||
private String generateUniqueQrcodeCode()
|
||||
{
|
||||
String code;
|
||||
int maxAttempts = 10; // 最多尝试10次
|
||||
int attempts = 0;
|
||||
|
||||
do
|
||||
{
|
||||
code = IdUtils.fastSimpleUUID();
|
||||
attempts++;
|
||||
// 检查编码是否唯一(只检查未删除的记录)
|
||||
// 如果已存在且尝试次数未超过限制,继续生成
|
||||
if (attempts >= maxAttempts)
|
||||
{
|
||||
// 如果10次都重复(概率极低),使用时间戳+随机数
|
||||
code = "QR" + System.currentTimeMillis() + IdUtils.fastSimpleUUID().substring(0, 8);
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (!checkQrcodeCodeUnique(code));
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改二维码
|
||||
*
|
||||
|
|
@ -136,6 +199,20 @@ public class PsyQrcodeServiceImpl implements IPsyQrcodeService
|
|||
return count == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查二维码编码是否唯一(编辑时使用,排除自己)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @param qrcodeId 二维码ID(排除此ID)
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean checkQrcodeCodeUnique(String qrcodeCode, Long qrcodeId)
|
||||
{
|
||||
int count = qrcodeMapper.checkQrcodeCodeUniqueExcludeSelf(qrcodeCode, qrcodeId);
|
||||
return count == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码
|
||||
*
|
||||
|
|
@ -174,16 +251,24 @@ public class PsyQrcodeServiceImpl implements IPsyQrcodeService
|
|||
{
|
||||
if (StringUtils.isEmpty(qrcode.getShortUrl()))
|
||||
{
|
||||
// 如果没有短链接,直接构建完整URL
|
||||
// 如果没有短链接,构建完整URL
|
||||
// 由于Service层无法直接获取HTTP请求信息,使用配置的服务器地址
|
||||
StringBuilder url = new StringBuilder();
|
||||
url.append("http://localhost:");
|
||||
|
||||
// 判断是HTTP还是HTTPS(可以通过配置获取,这里默认HTTP)
|
||||
url.append("http://");
|
||||
url.append(serverAddress);
|
||||
url.append(":");
|
||||
url.append(serverPort);
|
||||
|
||||
if (StringUtils.isNotEmpty(contextPath))
|
||||
{
|
||||
url.append(contextPath);
|
||||
}
|
||||
|
||||
url.append("/psychology/qrcode/scan/");
|
||||
url.append(qrcode.getQrcodeCode());
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -0,0 +1,817 @@
|
|||
package com.ddnai.system.service.impl.psychology;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireItem;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireOption;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireAnswerMapper;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireAnswerDetailMapper;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireItemMapper;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireReportMapper;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireReport;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireAnswerService;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireOptionService;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireService;
|
||||
import com.ddnai.common.utils.SecurityUtils;
|
||||
import com.ddnai.common.utils.DateUtils;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaire;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* 问卷答题记录表 服务层实现
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@Service
|
||||
public class PsyQuestionnaireAnswerServiceImpl implements IPsyQuestionnaireAnswerService
|
||||
{
|
||||
@Autowired
|
||||
private PsyQuestionnaireAnswerMapper answerMapper;
|
||||
|
||||
@Autowired
|
||||
private PsyQuestionnaireAnswerDetailMapper detailMapper;
|
||||
|
||||
@Autowired
|
||||
private PsyQuestionnaireItemMapper itemMapper;
|
||||
|
||||
@Autowired
|
||||
private IPsyQuestionnaireOptionService optionService;
|
||||
|
||||
@Autowired
|
||||
private IPsyQuestionnaireService questionnaireService;
|
||||
|
||||
@Autowired
|
||||
private PsyQuestionnaireReportMapper reportMapper;
|
||||
|
||||
/**
|
||||
* 查询答题记录信息
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答题记录信息
|
||||
*/
|
||||
@Override
|
||||
public PsyQuestionnaireAnswer selectAnswerById(Long answerId)
|
||||
{
|
||||
return answerMapper.selectAnswerById(answerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询答题记录列表
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 答题记录集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireAnswer> selectAnswerList(PsyQuestionnaireAnswer answer)
|
||||
{
|
||||
return answerMapper.selectAnswerList(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertAnswer(PsyQuestionnaireAnswer answer)
|
||||
{
|
||||
return answerMapper.insertAnswer(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateAnswer(PsyQuestionnaireAnswer answer)
|
||||
{
|
||||
return answerMapper.updateAnswer(answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除答题记录
|
||||
*
|
||||
* @param answerIds 需要删除的答题ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteAnswerByIds(Long[] answerIds)
|
||||
{
|
||||
return answerMapper.deleteAnswerByIds(answerIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存或更新答案详情
|
||||
*
|
||||
* @param detail 答案详情
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int saveOrUpdateAnswerDetail(PsyQuestionnaireAnswerDetail detail)
|
||||
{
|
||||
// 检查是否已存在
|
||||
PsyQuestionnaireAnswerDetail existing = detailMapper.selectDetailByAnswerIdAndItemId(
|
||||
detail.getAnswerId(), detail.getItemId());
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// 更新
|
||||
detail.setDetailId(existing.getDetailId());
|
||||
return detailMapper.updateDetail(detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新增
|
||||
return detailMapper.insertDetail(detail);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询答案详情列表
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答案详情集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireAnswerDetail> selectDetailListByAnswerId(Long answerId)
|
||||
{
|
||||
return detailMapper.selectDetailListByAnswerId(answerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交问卷(自动评分)
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public int submitAnswer(Long answerId)
|
||||
{
|
||||
// 获取答题记录
|
||||
PsyQuestionnaireAnswer answer = answerMapper.selectAnswerById(answerId);
|
||||
if (answer == null || !"0".equals(answer.getStatus()))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 获取所有答案详情
|
||||
List<PsyQuestionnaireAnswerDetail> details = detailMapper.selectDetailListByAnswerId(answerId);
|
||||
|
||||
// 自动评分客观题
|
||||
BigDecimal totalScore = BigDecimal.ZERO;
|
||||
for (PsyQuestionnaireAnswerDetail detail : details)
|
||||
{
|
||||
// 获取题目信息
|
||||
PsyQuestionnaireItem item = itemMapper.selectItemById(detail.getItemId());
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断是否为主观题
|
||||
boolean isSubjective = isSubjectiveType(item.getItemType());
|
||||
detail.setIsSubjective(isSubjective ? "1" : "0");
|
||||
|
||||
if (!isSubjective)
|
||||
{
|
||||
// 客观题自动评分
|
||||
BigDecimal score = calculateObjectiveScore(item, detail);
|
||||
detail.setAnswerScore(score);
|
||||
detail.setIsScored("1");
|
||||
totalScore = totalScore.add(score);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 主观题标记为待评分
|
||||
detail.setIsScored("0");
|
||||
}
|
||||
|
||||
// 更新答案详情
|
||||
detailMapper.updateDetail(detail);
|
||||
}
|
||||
|
||||
// 更新答题记录
|
||||
answer.setTotalScore(totalScore);
|
||||
answer.setSubmitTime(new Date());
|
||||
answer.setStatus("1"); // 已完成
|
||||
answer.setUpdateBy(SecurityUtils.getUsername());
|
||||
|
||||
// 判断是否及格(需要从问卷表获取及格分数)
|
||||
// 这里暂时不判断,后续可以完善
|
||||
|
||||
// 更新排名(需要重新计算所有答题记录的排名)
|
||||
updateRank(answer.getQuestionnaireId());
|
||||
|
||||
int result = answerMapper.updateAnswer(answer);
|
||||
|
||||
// 生成问卷报告
|
||||
if (result > 0) {
|
||||
try {
|
||||
generateQuestionnaireReport(answerId);
|
||||
System.out.println("问卷报告生成完成,answerId: " + answerId);
|
||||
} catch (Exception e) {
|
||||
// 报告生成失败不影响提交,但记录详细错误
|
||||
System.err.println("生成问卷报告失败,answerId: " + answerId);
|
||||
System.err.println("错误信息: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
// 不抛出异常,让提交流程继续
|
||||
}
|
||||
} else {
|
||||
System.err.println("更新答题记录失败,无法生成报告,answerId: " + answerId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成问卷报告(公共方法)
|
||||
*/
|
||||
@Override
|
||||
public void generateReport(Long answerId)
|
||||
{
|
||||
generateQuestionnaireReport(answerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成问卷报告(私有实现方法)
|
||||
*/
|
||||
private void generateQuestionnaireReport(Long answerId)
|
||||
{
|
||||
try {
|
||||
System.out.println("开始生成问卷报告,answerId: " + answerId);
|
||||
|
||||
// 获取答题记录
|
||||
PsyQuestionnaireAnswer answer = answerMapper.selectAnswerById(answerId);
|
||||
if (answer == null) {
|
||||
System.err.println("答题记录不存在,answerId: " + answerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取问卷信息
|
||||
PsyQuestionnaire questionnaire = questionnaireService.selectQuestionnaireById(answer.getQuestionnaireId());
|
||||
if (questionnaire == null) {
|
||||
System.err.println("问卷不存在,questionnaireId: " + answer.getQuestionnaireId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有答案详情
|
||||
List<PsyQuestionnaireAnswerDetail> details = detailMapper.selectDetailListByAnswerId(answerId);
|
||||
if (details == null || details.isEmpty()) {
|
||||
System.err.println("答案详情为空,answerId: " + answerId);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("获取到 " + details.size() + " 条答案详情");
|
||||
|
||||
// 生成报告内容
|
||||
StringBuilder reportContent = new StringBuilder();
|
||||
reportContent.append("<div class='report-container'>");
|
||||
// HTML转义问卷名称
|
||||
String questionnaireName = questionnaire.getQuestionnaireName() != null ? questionnaire.getQuestionnaireName() : "";
|
||||
questionnaireName = questionnaireName.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||
reportContent.append("<h1>").append(questionnaireName).append(" - 答题报告</h1>");
|
||||
reportContent.append("<p class='report-info'>答题时间:").append(answer.getStartTime() != null ? DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, answer.getStartTime()) : "").append("</p>");
|
||||
if (answer.getRespondentName() != null && !answer.getRespondentName().isEmpty()) {
|
||||
// HTML转义答题人姓名
|
||||
String respondentName = answer.getRespondentName().replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||
reportContent.append("<p class='report-info'>答题人:").append(respondentName).append("</p>");
|
||||
}
|
||||
reportContent.append("<p class='report-info'>总题目数:").append(questionnaire.getItemCount() != null ? questionnaire.getItemCount() : 0).append("</p>");
|
||||
reportContent.append("<p class='report-info'>总得分:").append(answer.getTotalScore() != null ? answer.getTotalScore() : BigDecimal.ZERO).append("</p>");
|
||||
if (answer.getRank() != null) {
|
||||
reportContent.append("<p class='report-info'>排名:第").append(answer.getRank()).append("名</p>");
|
||||
}
|
||||
|
||||
// 题目得分详情
|
||||
reportContent.append("<h2>题目得分详情</h2>");
|
||||
reportContent.append("<table class='score-table' style='width: 100%; border-collapse: collapse; margin: 20px 0;'>");
|
||||
reportContent.append("<thead><tr style='background-color: #f5f7fa;'>");
|
||||
reportContent.append("<th style='padding: 10px; border: 1px solid #ddd;'>题目序号</th>");
|
||||
reportContent.append("<th style='padding: 10px; border: 1px solid #ddd;'>题目内容</th>");
|
||||
reportContent.append("<th style='padding: 10px; border: 1px solid #ddd;'>得分</th>");
|
||||
reportContent.append("<th style='padding: 10px; border: 1px solid #ddd;'>状态</th>");
|
||||
reportContent.append("</tr></thead><tbody>");
|
||||
|
||||
for (PsyQuestionnaireAnswerDetail detail : details) {
|
||||
PsyQuestionnaireItem item = itemMapper.selectItemById(detail.getItemId());
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
reportContent.append("<tr>");
|
||||
reportContent.append("<td style='padding: 10px; border: 1px solid #ddd;'>").append(item.getItemNumber() != null ? item.getItemNumber() : "").append("</td>");
|
||||
// HTML转义题目内容,防止XSS攻击
|
||||
String itemContent = item.getItemContent() != null ? item.getItemContent() : "";
|
||||
itemContent = itemContent.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||
reportContent.append("<td style='padding: 10px; border: 1px solid #ddd;'>").append(itemContent).append("</td>");
|
||||
reportContent.append("<td style='padding: 10px; border: 1px solid #ddd; text-align: center;'>").append(detail.getAnswerScore() != null ? detail.getAnswerScore() : BigDecimal.ZERO).append("</td>");
|
||||
String statusText = "1".equals(detail.getIsScored()) ? "已评分" : ("1".equals(detail.getIsSubjective()) ? "待评分" : "未评分");
|
||||
reportContent.append("<td style='padding: 10px; border: 1px solid #ddd;'>").append(statusText).append("</td>");
|
||||
reportContent.append("</tr>");
|
||||
}
|
||||
|
||||
reportContent.append("</tbody></table>");
|
||||
reportContent.append("</div>");
|
||||
|
||||
// 生成报告摘要
|
||||
String summary = String.format("本次答题共完成%d道题目,总得分%.2f分",
|
||||
details.size(),
|
||||
answer.getTotalScore() != null ? answer.getTotalScore().doubleValue() : 0.0);
|
||||
|
||||
// 保存报告到数据库
|
||||
PsyQuestionnaireReport existingReport = reportMapper.selectReportByAnswerId(answerId);
|
||||
PsyQuestionnaireReport report;
|
||||
|
||||
// HTML转义报告标题
|
||||
String reportTitle = (questionnaire.getQuestionnaireName() != null ? questionnaire.getQuestionnaireName() : "") + " - 答题报告";
|
||||
reportTitle = reportTitle.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||
|
||||
if (existingReport != null) {
|
||||
// 更新现有报告
|
||||
System.out.println("更新已存在的报告,reportId: " + existingReport.getReportId());
|
||||
report = existingReport;
|
||||
report.setReportType("standard");
|
||||
report.setReportTitle(reportTitle);
|
||||
report.setReportContent(reportContent.toString());
|
||||
report.setSummary(summary);
|
||||
report.setIsGenerated("1");
|
||||
report.setGenerateTime(DateUtils.getNowDate());
|
||||
report.setUpdateBy(SecurityUtils.getUsername());
|
||||
report.setUpdateTime(DateUtils.getNowDate());
|
||||
int updateResult = reportMapper.updateReport(report);
|
||||
System.out.println("更新报告结果: " + updateResult + ", reportId: " + report.getReportId());
|
||||
} else {
|
||||
// 创建新报告
|
||||
report = new PsyQuestionnaireReport();
|
||||
report.setAnswerId(answerId);
|
||||
report.setReportType("standard");
|
||||
report.setReportTitle(reportTitle);
|
||||
report.setReportContent(reportContent.toString());
|
||||
report.setSummary(summary);
|
||||
report.setIsGenerated("1");
|
||||
report.setGenerateTime(DateUtils.getNowDate());
|
||||
report.setCreateBy(SecurityUtils.getUsername());
|
||||
report.setCreateTime(DateUtils.getNowDate());
|
||||
int insertResult = reportMapper.insertReport(report);
|
||||
System.out.println("创建新报告结果: " + insertResult + ", reportId: " + report.getReportId());
|
||||
if (insertResult <= 0) {
|
||||
System.err.println("警告:报告插入失败,insertResult: " + insertResult);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("报告内容长度: " + reportContent.length());
|
||||
System.out.println("报告摘要: " + summary);
|
||||
|
||||
System.out.println("问卷报告生成成功,answerId: " + answerId + ", reportId: " + report.getReportId());
|
||||
} catch (Exception e) {
|
||||
System.err.println("生成问卷报告时发生异常,answerId: " + answerId);
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("生成问卷报告失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为主观题类型
|
||||
*/
|
||||
private boolean isSubjectiveType(String itemType)
|
||||
{
|
||||
if (itemType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 主观题类型:text(简答)、textarea(问答)、essay(作文)
|
||||
return "text".equals(itemType) || "textarea".equals(itemType) || "essay".equals(itemType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算客观题得分
|
||||
*/
|
||||
private BigDecimal calculateObjectiveScore(PsyQuestionnaireItem item, PsyQuestionnaireAnswerDetail detail)
|
||||
{
|
||||
String itemType = item.getItemType();
|
||||
if (itemType == null)
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 获取题目的所有选项
|
||||
List<PsyQuestionnaireOption> allOptions = optionService.selectOptionListByItemId(item.getItemId());
|
||||
if (allOptions == null || allOptions.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 获取正确答案选项
|
||||
List<PsyQuestionnaireOption> correctOptions = optionService.selectCorrectOptionListByItemId(item.getItemId());
|
||||
Set<Long> correctOptionIds = correctOptions.stream()
|
||||
.map(PsyQuestionnaireOption::getOptionId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 题目分值(如果没有设置,使用选项分值总和)
|
||||
BigDecimal itemScore = item.getScore();
|
||||
if (itemScore == null || itemScore.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
// 计算所有正确答案的分值总和
|
||||
itemScore = correctOptions.stream()
|
||||
.map(opt -> opt.getOptionScore() != null ? opt.getOptionScore() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
// 如果还是0,使用题目的默认分值
|
||||
if (itemScore.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
itemScore = BigDecimal.ONE;
|
||||
}
|
||||
}
|
||||
|
||||
switch (itemType)
|
||||
{
|
||||
case "radio": // 单选
|
||||
return calculateRadioScore(detail, correctOptionIds, itemScore, allOptions);
|
||||
|
||||
case "checkbox": // 多选
|
||||
return calculateCheckboxScore(detail, correctOptionIds, itemScore, allOptions);
|
||||
|
||||
case "boolean": // 判断
|
||||
return calculateBooleanScore(detail, correctOptionIds, itemScore, allOptions);
|
||||
|
||||
case "input": // 填空
|
||||
return calculateInputScore(detail, correctOptions, itemScore);
|
||||
|
||||
case "sort": // 排序
|
||||
return calculateSortScore(detail, correctOptions, itemScore);
|
||||
|
||||
case "calculate": // 计算
|
||||
return calculateCalculateScore(detail, correctOptions, itemScore);
|
||||
|
||||
default:
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单选题得分
|
||||
*/
|
||||
private BigDecimal calculateRadioScore(PsyQuestionnaireAnswerDetail detail, Set<Long> correctOptionIds,
|
||||
BigDecimal itemScore, List<PsyQuestionnaireOption> allOptions)
|
||||
{
|
||||
if (detail.getOptionId() == null)
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 如果选中的是正确答案,得满分
|
||||
if (correctOptionIds.contains(detail.getOptionId()))
|
||||
{
|
||||
// 查找选中选项的分值
|
||||
PsyQuestionnaireOption selectedOption = allOptions.stream()
|
||||
.filter(opt -> opt.getOptionId().equals(detail.getOptionId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (selectedOption != null && selectedOption.getOptionScore() != null)
|
||||
{
|
||||
return selectedOption.getOptionScore();
|
||||
}
|
||||
return itemScore;
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多选题得分
|
||||
*/
|
||||
private BigDecimal calculateCheckboxScore(PsyQuestionnaireAnswerDetail detail, Set<Long> correctOptionIds,
|
||||
BigDecimal itemScore, List<PsyQuestionnaireOption> allOptions)
|
||||
{
|
||||
if (detail.getOptionIds() == null || detail.getOptionIds().trim().isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 解析用户选择的选项ID
|
||||
Set<Long> selectedOptionIds = Arrays.stream(detail.getOptionIds().split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Long::valueOf)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (selectedOptionIds.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 计算选对的选项数量
|
||||
Set<Long> correctSelected = new HashSet<>(selectedOptionIds);
|
||||
correctSelected.retainAll(correctOptionIds);
|
||||
|
||||
// 计算选错的选项数量
|
||||
Set<Long> wrongSelected = new HashSet<>(selectedOptionIds);
|
||||
wrongSelected.removeAll(correctOptionIds);
|
||||
|
||||
// 如果全对,得满分
|
||||
if (wrongSelected.isEmpty() && correctSelected.size() == correctOptionIds.size())
|
||||
{
|
||||
return itemScore;
|
||||
}
|
||||
|
||||
// 如果部分对,按比例得分
|
||||
if (correctSelected.size() > 0)
|
||||
{
|
||||
// 计算选对选项的分值总和
|
||||
BigDecimal correctScore = allOptions.stream()
|
||||
.filter(opt -> correctSelected.contains(opt.getOptionId()))
|
||||
.map(opt -> opt.getOptionScore() != null ? opt.getOptionScore() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 如果有选错的,扣分(可选:按错选数量扣分)
|
||||
if (!wrongSelected.isEmpty())
|
||||
{
|
||||
// 计算错选选项的分值总和(作为扣分)
|
||||
BigDecimal wrongScore = allOptions.stream()
|
||||
.filter(opt -> wrongSelected.contains(opt.getOptionId()))
|
||||
.map(opt -> opt.getOptionScore() != null ? opt.getOptionScore() : BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 扣分策略:错选扣分,但最低为0
|
||||
correctScore = correctScore.subtract(wrongScore);
|
||||
if (correctScore.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
correctScore = BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
return correctScore;
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算判断题得分
|
||||
*/
|
||||
private BigDecimal calculateBooleanScore(PsyQuestionnaireAnswerDetail detail, Set<Long> correctOptionIds,
|
||||
BigDecimal itemScore, List<PsyQuestionnaireOption> allOptions)
|
||||
{
|
||||
if (detail.getOptionId() == null)
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 如果选中的是正确答案,得满分
|
||||
if (correctOptionIds.contains(detail.getOptionId()))
|
||||
{
|
||||
// 查找选中选项的分值
|
||||
PsyQuestionnaireOption selectedOption = allOptions.stream()
|
||||
.filter(opt -> opt.getOptionId().equals(detail.getOptionId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (selectedOption != null && selectedOption.getOptionScore() != null)
|
||||
{
|
||||
return selectedOption.getOptionScore();
|
||||
}
|
||||
return itemScore;
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算填空题得分(文本匹配)
|
||||
*/
|
||||
private BigDecimal calculateInputScore(PsyQuestionnaireAnswerDetail detail, List<PsyQuestionnaireOption> correctOptions,
|
||||
BigDecimal itemScore)
|
||||
{
|
||||
if (detail.getAnswerText() == null || detail.getAnswerText().trim().isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
String userAnswer = detail.getAnswerText().trim().toLowerCase();
|
||||
|
||||
// 如果没有标准答案,返回0(需要人工评分)
|
||||
if (correctOptions == null || correctOptions.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 检查是否匹配任一标准答案
|
||||
for (PsyQuestionnaireOption correctOption : correctOptions)
|
||||
{
|
||||
String correctAnswer = correctOption.getOptionContent();
|
||||
if (correctAnswer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 精确匹配
|
||||
if (userAnswer.equals(correctAnswer.trim().toLowerCase()))
|
||||
{
|
||||
return correctOption.getOptionScore() != null ? correctOption.getOptionScore() : itemScore;
|
||||
}
|
||||
|
||||
// 模糊匹配(包含关系)
|
||||
if (userAnswer.contains(correctAnswer.trim().toLowerCase()) ||
|
||||
correctAnswer.trim().toLowerCase().contains(userAnswer))
|
||||
{
|
||||
// 模糊匹配给部分分(50%)
|
||||
BigDecimal score = correctOption.getOptionScore() != null ? correctOption.getOptionScore() : itemScore;
|
||||
return score.multiply(new BigDecimal("0.5"));
|
||||
}
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算排序题得分
|
||||
*/
|
||||
private BigDecimal calculateSortScore(PsyQuestionnaireAnswerDetail detail, List<PsyQuestionnaireOption> correctOptions,
|
||||
BigDecimal itemScore)
|
||||
{
|
||||
if (detail.getOptionIds() == null || detail.getOptionIds().trim().isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 解析用户排序的选项ID列表
|
||||
List<Long> userOrder = Arrays.stream(detail.getOptionIds().split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.map(Long::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取正确答案的排序(按sort_order排序)
|
||||
List<Long> correctOrder = correctOptions.stream()
|
||||
.sorted((a, b) -> Integer.compare(
|
||||
a.getSortOrder() != null ? a.getSortOrder() : 0,
|
||||
b.getSortOrder() != null ? b.getSortOrder() : 0))
|
||||
.map(PsyQuestionnaireOption::getOptionId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (userOrder.size() != correctOrder.size())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 检查顺序是否完全正确
|
||||
boolean isCorrect = true;
|
||||
for (int i = 0; i < userOrder.size(); i++)
|
||||
{
|
||||
if (!userOrder.get(i).equals(correctOrder.get(i)))
|
||||
{
|
||||
isCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCorrect)
|
||||
{
|
||||
return itemScore;
|
||||
}
|
||||
|
||||
// 部分正确:计算正确位置的数量
|
||||
int correctCount = 0;
|
||||
for (int i = 0; i < userOrder.size(); i++)
|
||||
{
|
||||
if (userOrder.get(i).equals(correctOrder.get(i)))
|
||||
{
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 按比例得分
|
||||
if (correctCount > 0)
|
||||
{
|
||||
BigDecimal ratio = new BigDecimal(correctCount).divide(new BigDecimal(correctOrder.size()), 4, RoundingMode.HALF_UP);
|
||||
return itemScore.multiply(ratio);
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算计算题得分(数值匹配,允许误差)
|
||||
*/
|
||||
private BigDecimal calculateCalculateScore(PsyQuestionnaireAnswerDetail detail, List<PsyQuestionnaireOption> correctOptions,
|
||||
BigDecimal itemScore)
|
||||
{
|
||||
if (detail.getAnswerText() == null || detail.getAnswerText().trim().isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 解析用户答案(尝试转换为数字)
|
||||
BigDecimal userAnswer = new BigDecimal(detail.getAnswerText().trim());
|
||||
|
||||
// 如果没有标准答案,返回0
|
||||
if (correctOptions == null || correctOptions.isEmpty())
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 检查是否匹配任一标准答案(允许误差范围)
|
||||
for (PsyQuestionnaireOption correctOption : correctOptions)
|
||||
{
|
||||
String correctAnswerStr = correctOption.getOptionContent();
|
||||
if (correctAnswerStr == null || correctAnswerStr.trim().isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
BigDecimal correctAnswer = new BigDecimal(correctAnswerStr.trim());
|
||||
|
||||
// 计算误差(默认允许5%的误差)
|
||||
BigDecimal tolerance = correctAnswer.multiply(new BigDecimal("0.05"));
|
||||
BigDecimal diff = userAnswer.subtract(correctAnswer).abs();
|
||||
|
||||
// 如果误差在允许范围内,得满分
|
||||
if (diff.compareTo(tolerance) <= 0)
|
||||
{
|
||||
return correctOption.getOptionScore() != null ? correctOption.getOptionScore() : itemScore;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// 标准答案不是数字,跳过
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// 用户答案不是数字,返回0
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新排名
|
||||
*/
|
||||
private void updateRank(Long questionnaireId)
|
||||
{
|
||||
// 查询该问卷的所有答题记录,按总分降序排序
|
||||
PsyQuestionnaireAnswer query = new PsyQuestionnaireAnswer();
|
||||
query.setQuestionnaireId(questionnaireId);
|
||||
query.setStatus("1"); // 只统计已完成的
|
||||
List<PsyQuestionnaireAnswer> answers = answerMapper.selectAnswerList(query);
|
||||
|
||||
// 按总分降序排序
|
||||
answers.sort((a, b) -> {
|
||||
if (a.getTotalScore() == null && b.getTotalScore() == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (a.getTotalScore() == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (b.getTotalScore() == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int compare = b.getTotalScore().compareTo(a.getTotalScore());
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
// 总分相同,按提交时间排序(先提交的排名靠前)
|
||||
if (a.getSubmitTime() != null && b.getSubmitTime() != null)
|
||||
{
|
||||
return a.getSubmitTime().compareTo(b.getSubmitTime());
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 更新排名
|
||||
int rank = 1;
|
||||
for (PsyQuestionnaireAnswer answer : answers)
|
||||
{
|
||||
answer.setRank(rank++);
|
||||
answer.setUpdateBy(SecurityUtils.getUsername());
|
||||
answerMapper.updateAnswer(answer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.ddnai.system.service.impl.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireItem;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireItemMapper;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireItemService;
|
||||
|
||||
/**
|
||||
* 问卷题目表 服务层实现
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@Service
|
||||
public class PsyQuestionnaireItemServiceImpl implements IPsyQuestionnaireItemService
|
||||
{
|
||||
@Autowired
|
||||
private PsyQuestionnaireItemMapper itemMapper;
|
||||
|
||||
/**
|
||||
* 查询题目信息
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 题目信息
|
||||
*/
|
||||
@Override
|
||||
public PsyQuestionnaireItem selectItemById(Long itemId)
|
||||
{
|
||||
return itemMapper.selectItemById(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询问卷题目列表
|
||||
*
|
||||
* @param questionnaireId 问卷ID
|
||||
* @return 题目集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireItem> selectItemListByQuestionnaireId(Long questionnaireId)
|
||||
{
|
||||
return itemMapper.selectItemListByQuestionnaireId(questionnaireId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询题目列表
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 题目集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireItem> selectItemList(PsyQuestionnaireItem item)
|
||||
{
|
||||
return itemMapper.selectItemList(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增题目
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertItem(PsyQuestionnaireItem item)
|
||||
{
|
||||
return itemMapper.insertItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改题目
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateItem(PsyQuestionnaireItem item)
|
||||
{
|
||||
return itemMapper.updateItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除题目
|
||||
*
|
||||
* @param itemIds 需要删除的题目ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteItemByIds(Long[] itemIds)
|
||||
{
|
||||
return itemMapper.deleteItemByIds(itemIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目信息
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteItemById(Long itemId)
|
||||
{
|
||||
return itemMapper.deleteItemById(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.ddnai.system.service.impl.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireOption;
|
||||
import com.ddnai.system.mapper.psychology.PsyQuestionnaireOptionMapper;
|
||||
import com.ddnai.system.service.psychology.IPsyQuestionnaireOptionService;
|
||||
|
||||
/**
|
||||
* 问卷选项表 服务层实现
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
@Service
|
||||
public class PsyQuestionnaireOptionServiceImpl implements IPsyQuestionnaireOptionService
|
||||
{
|
||||
@Autowired
|
||||
private PsyQuestionnaireOptionMapper optionMapper;
|
||||
|
||||
/**
|
||||
* 查询选项信息
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 选项信息
|
||||
*/
|
||||
@Override
|
||||
public PsyQuestionnaireOption selectOptionById(Long optionId)
|
||||
{
|
||||
return optionMapper.selectOptionById(optionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询题目选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 选项集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireOption> selectOptionListByItemId(Long itemId)
|
||||
{
|
||||
return optionMapper.selectOptionListByItemId(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正确答案选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 正确答案选项集合
|
||||
*/
|
||||
@Override
|
||||
public List<PsyQuestionnaireOption> selectCorrectOptionListByItemId(Long itemId)
|
||||
{
|
||||
return optionMapper.selectCorrectOptionListByItemId(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int insertOption(PsyQuestionnaireOption option)
|
||||
{
|
||||
return optionMapper.insertOption(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateOption(PsyQuestionnaireOption option)
|
||||
{
|
||||
return optionMapper.updateOption(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除选项
|
||||
*
|
||||
* @param optionIds 需要删除的选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteOptionByIds(Long[] optionIds)
|
||||
{
|
||||
return optionMapper.deleteOptionByIds(optionIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除选项信息
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteOptionById(Long optionId)
|
||||
{
|
||||
return optionMapper.deleteOptionById(optionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import com.ddnai.system.domain.psychology.PsyFactorRule;
|
|||
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
|
||||
import com.ddnai.system.domain.psychology.PsyWarningRule;
|
||||
import com.ddnai.system.domain.psychology.vo.ScaleImportVO;
|
||||
import com.ddnai.system.domain.psychology.vo.InterpretationImportVO;
|
||||
import com.ddnai.system.mapper.psychology.PsyScaleMapper;
|
||||
import com.ddnai.system.service.psychology.IPsyScaleService;
|
||||
import com.ddnai.system.service.psychology.IPsyFactorService;
|
||||
|
|
@ -522,7 +523,7 @@ public class PsyScaleServiceImpl implements IPsyScaleService
|
|||
// 从原始JSON中提取factorCode信息并映射到factorId
|
||||
for (int i = 0; i < importData.getInterpretations().size(); i++)
|
||||
{
|
||||
PsyResultInterpretation interpretation = importData.getInterpretations().get(i);
|
||||
InterpretationImportVO interpretation = importData.getInterpretations().get(i);
|
||||
interpretation.setScaleId(scaleId);
|
||||
|
||||
// 如果factorId为null,尝试从原始JSON中获取factorCode并映射
|
||||
|
|
@ -563,7 +564,12 @@ public class PsyScaleServiceImpl implements IPsyScaleService
|
|||
}
|
||||
try
|
||||
{
|
||||
interpretationService.insertInterpretation(interpretation);
|
||||
// 将InterpretationImportVO转换为PsyResultInterpretation
|
||||
PsyResultInterpretation interpretationEntity = interpretation.toPsyResultInterpretation();
|
||||
interpretationEntity.setScaleId(scaleId);
|
||||
interpretationEntity.setFactorId(interpretation.getFactorId());
|
||||
interpretationEntity.setCreateBy(username);
|
||||
interpretationService.insertInterpretation(interpretationEntity);
|
||||
interpretationCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -638,5 +644,169 @@ public class PsyScaleServiceImpl implements IPsyScaleService
|
|||
log.info("量表导入完成,scaleId: {}", scaleId);
|
||||
return scaleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出量表数据(JSON格式)
|
||||
*/
|
||||
@Override
|
||||
public List<ScaleImportVO> exportScales(Long[] scaleIds)
|
||||
{
|
||||
List<ScaleImportVO> exportList = new java.util.ArrayList<>();
|
||||
|
||||
for (Long scaleId : scaleIds)
|
||||
{
|
||||
ScaleImportVO exportData = new ScaleImportVO();
|
||||
|
||||
// 1. 获取量表基本信息
|
||||
PsyScale scale = scaleMapper.selectScaleById(scaleId);
|
||||
if (scale == null)
|
||||
{
|
||||
log.warn("量表不存在,scaleId: {}", scaleId);
|
||||
continue;
|
||||
}
|
||||
exportData.setScale(scale);
|
||||
|
||||
// 2. 获取因子列表和计分规则
|
||||
List<PsyFactor> factors = factorService.selectFactorListByScaleId(scaleId);
|
||||
List<ScaleImportVO.FactorImportVO> factorImportList = new java.util.ArrayList<>();
|
||||
Map<Long, String> factorIdToCodeMap = new HashMap<>(); // 因子ID到因子代码的映射,用于导出解释和预警规则
|
||||
|
||||
for (PsyFactor factor : factors)
|
||||
{
|
||||
// 建立因子ID到因子代码的映射
|
||||
if (factor.getFactorCode() != null && !factor.getFactorCode().isEmpty())
|
||||
{
|
||||
factorIdToCodeMap.put(factor.getFactorId(), factor.getFactorCode());
|
||||
}
|
||||
|
||||
ScaleImportVO.FactorImportVO factorImport = new ScaleImportVO.FactorImportVO();
|
||||
factorImport.setFactor(factor);
|
||||
|
||||
// 获取计分规则
|
||||
List<PsyFactorRule> rules = factorRuleService.selectRuleListByFactorId(factor.getFactorId());
|
||||
List<ScaleImportVO.FactorRuleImportVO> ruleImportList = new java.util.ArrayList<>();
|
||||
|
||||
for (PsyFactorRule rule : rules)
|
||||
{
|
||||
ScaleImportVO.FactorRuleImportVO ruleImport = new ScaleImportVO.FactorRuleImportVO();
|
||||
ruleImport.setRule(rule);
|
||||
|
||||
// 获取题目序号(通过itemId查找题目的itemNumber)
|
||||
PsyScaleItem item = itemService.selectItemById(rule.getItemId());
|
||||
if (item != null && item.getItemNumber() != null)
|
||||
{
|
||||
ruleImport.setItemNumber(item.getItemNumber());
|
||||
}
|
||||
else
|
||||
{
|
||||
log.warn("无法找到题目序号,itemId: {}", rule.getItemId());
|
||||
}
|
||||
|
||||
ruleImportList.add(ruleImport);
|
||||
}
|
||||
|
||||
factorImport.setRules(ruleImportList);
|
||||
factorImportList.add(factorImport);
|
||||
}
|
||||
exportData.setFactors(factorImportList);
|
||||
|
||||
// 3. 获取题目列表和选项
|
||||
List<PsyScaleItem> items = itemService.selectItemListByScaleId(scaleId);
|
||||
List<ScaleImportVO.ItemImportVO> itemImportList = new java.util.ArrayList<>();
|
||||
|
||||
for (PsyScaleItem item : items)
|
||||
{
|
||||
ScaleImportVO.ItemImportVO itemImport = new ScaleImportVO.ItemImportVO();
|
||||
itemImport.setItem(item);
|
||||
|
||||
// 获取选项列表
|
||||
List<PsyScaleOption> options = optionService.selectOptionListByItemId(item.getItemId());
|
||||
itemImport.setOptions(options != null ? options : new java.util.ArrayList<>());
|
||||
|
||||
itemImportList.add(itemImport);
|
||||
}
|
||||
exportData.setItems(itemImportList);
|
||||
|
||||
// 4. 获取解释配置(添加factorCode信息)
|
||||
PsyResultInterpretation interpretationQuery = new PsyResultInterpretation();
|
||||
interpretationQuery.setScaleId(scaleId);
|
||||
List<PsyResultInterpretation> interpretations = interpretationService.selectInterpretationList(interpretationQuery);
|
||||
List<com.ddnai.system.domain.psychology.vo.InterpretationImportVO> interpretationImportList = new java.util.ArrayList<>();
|
||||
if (interpretations != null)
|
||||
{
|
||||
for (PsyResultInterpretation interpretation : interpretations)
|
||||
{
|
||||
com.ddnai.system.domain.psychology.vo.InterpretationImportVO interpretationImport = new com.ddnai.system.domain.psychology.vo.InterpretationImportVO();
|
||||
interpretationImport.setScaleId(interpretation.getScaleId());
|
||||
interpretationImport.setFactorId(interpretation.getFactorId());
|
||||
interpretationImport.setScoreRangeMin(interpretation.getScoreRangeMin());
|
||||
interpretationImport.setScoreRangeMax(interpretation.getScoreRangeMax());
|
||||
interpretationImport.setLevel(interpretation.getLevel());
|
||||
interpretationImport.setLevelName(interpretation.getLevelName());
|
||||
interpretationImport.setInterpretationTitle(interpretation.getInterpretationTitle());
|
||||
interpretationImport.setInterpretationContent(interpretation.getInterpretationContent());
|
||||
interpretationImport.setSuggestions(interpretation.getSuggestions());
|
||||
interpretationImport.setSortOrder(interpretation.getSortOrder());
|
||||
interpretationImport.setCreateBy(interpretation.getCreateBy());
|
||||
interpretationImport.setCreateTime(interpretation.getCreateTime());
|
||||
interpretationImport.setUpdateBy(interpretation.getUpdateBy());
|
||||
interpretationImport.setUpdateTime(interpretation.getUpdateTime());
|
||||
|
||||
// 添加factorCode(如果有factorId)
|
||||
if (interpretation.getFactorId() != null && factorIdToCodeMap.containsKey(interpretation.getFactorId()))
|
||||
{
|
||||
interpretationImport.setFactorCode(factorIdToCodeMap.get(interpretation.getFactorId()));
|
||||
}
|
||||
|
||||
interpretationImportList.add(interpretationImport);
|
||||
}
|
||||
}
|
||||
// 注意:由于ScaleImportVO使用List<PsyResultInterpretation>,我们需要使用反射或类型转换
|
||||
// 这里我们直接设置,Jackson会自动处理
|
||||
exportData.setInterpretations(new java.util.ArrayList<>(interpretationImportList));
|
||||
|
||||
// 5. 获取预警规则(添加factorCode信息)
|
||||
PsyWarningRule warningRuleQuery = new PsyWarningRule();
|
||||
warningRuleQuery.setScaleId(scaleId);
|
||||
List<PsyWarningRule> warningRules = warningRuleService.selectWarningRuleList(warningRuleQuery);
|
||||
List<com.ddnai.system.domain.psychology.vo.WarningRuleImportVO> warningRuleImportList = new java.util.ArrayList<>();
|
||||
if (warningRules != null)
|
||||
{
|
||||
for (PsyWarningRule warningRule : warningRules)
|
||||
{
|
||||
com.ddnai.system.domain.psychology.vo.WarningRuleImportVO warningRuleImport = new com.ddnai.system.domain.psychology.vo.WarningRuleImportVO();
|
||||
warningRuleImport.setScaleId(warningRule.getScaleId());
|
||||
warningRuleImport.setFactorId(warningRule.getFactorId());
|
||||
warningRuleImport.setRuleName(warningRule.getRuleName());
|
||||
warningRuleImport.setWarningLevel(warningRule.getWarningLevel());
|
||||
warningRuleImport.setScoreMin(warningRule.getScoreMin());
|
||||
warningRuleImport.setScoreMax(warningRule.getScoreMax());
|
||||
warningRuleImport.setPercentileMin(warningRule.getPercentileMin());
|
||||
warningRuleImport.setPercentileMax(warningRule.getPercentileMax());
|
||||
warningRuleImport.setAutoRelief(warningRule.getAutoRelief());
|
||||
warningRuleImport.setReliefCondition(warningRule.getReliefCondition());
|
||||
warningRuleImport.setStatus(warningRule.getStatus());
|
||||
warningRuleImport.setCreateBy(warningRule.getCreateBy());
|
||||
warningRuleImport.setCreateTime(warningRule.getCreateTime());
|
||||
warningRuleImport.setUpdateBy(warningRule.getUpdateBy());
|
||||
warningRuleImport.setUpdateTime(warningRule.getUpdateTime());
|
||||
|
||||
// 添加factorCode(如果有factorId)
|
||||
if (warningRule.getFactorId() != null && factorIdToCodeMap.containsKey(warningRule.getFactorId()))
|
||||
{
|
||||
warningRuleImport.setFactorCode(factorIdToCodeMap.get(warningRule.getFactorId()));
|
||||
}
|
||||
|
||||
warningRuleImportList.add(warningRuleImport);
|
||||
}
|
||||
}
|
||||
// 同样处理预警规则
|
||||
exportData.setWarningRules(new java.util.ArrayList<>(warningRuleImportList));
|
||||
|
||||
exportList.add(exportData);
|
||||
}
|
||||
|
||||
return exportList;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,22 @@ public interface IPsyQrcodeService
|
|||
*/
|
||||
public PsyQrcode selectQrcodeById(Long qrcodeId);
|
||||
|
||||
/**
|
||||
* 根据编码查询二维码信息
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @return 二维码信息
|
||||
*/
|
||||
public PsyQrcode selectQrcodeByCode(String qrcodeCode);
|
||||
|
||||
/**
|
||||
* 扫描二维码(增加扫码次数)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @return 二维码信息
|
||||
*/
|
||||
public PsyQrcode scanQrcode(String qrcodeCode);
|
||||
|
||||
/**
|
||||
* 查询二维码列表
|
||||
*
|
||||
|
|
@ -58,6 +74,15 @@ public interface IPsyQrcodeService
|
|||
*/
|
||||
public boolean checkQrcodeCodeUnique(String qrcodeCode);
|
||||
|
||||
/**
|
||||
* 检查二维码编码是否唯一(编辑时使用,排除自己)
|
||||
*
|
||||
* @param qrcodeCode 二维码编码
|
||||
* @param qrcodeId 二维码ID(排除此ID)
|
||||
* @return 结果
|
||||
*/
|
||||
public boolean checkQrcodeCodeUnique(String qrcodeCode, Long qrcodeId);
|
||||
|
||||
/**
|
||||
* 生成二维码
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.ddnai.system.service.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail;
|
||||
|
||||
/**
|
||||
* 问卷答题记录表 服务层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface IPsyQuestionnaireAnswerService
|
||||
{
|
||||
/**
|
||||
* 查询答题记录信息
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答题记录信息
|
||||
*/
|
||||
public PsyQuestionnaireAnswer selectAnswerById(Long answerId);
|
||||
|
||||
/**
|
||||
* 查询答题记录列表
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 答题记录集合
|
||||
*/
|
||||
public List<PsyQuestionnaireAnswer> selectAnswerList(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 新增答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertAnswer(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 修改答题记录
|
||||
*
|
||||
* @param answer 答题记录信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateAnswer(PsyQuestionnaireAnswer answer);
|
||||
|
||||
/**
|
||||
* 批量删除答题记录
|
||||
*
|
||||
* @param answerIds 需要删除的答题ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteAnswerByIds(Long[] answerIds);
|
||||
|
||||
/**
|
||||
* 保存或更新答案详情
|
||||
*
|
||||
* @param detail 答案详情
|
||||
* @return 结果
|
||||
*/
|
||||
public int saveOrUpdateAnswerDetail(PsyQuestionnaireAnswerDetail detail);
|
||||
|
||||
/**
|
||||
* 查询答案详情列表
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 答案详情集合
|
||||
*/
|
||||
public List<PsyQuestionnaireAnswerDetail> selectDetailListByAnswerId(Long answerId);
|
||||
|
||||
/**
|
||||
* 提交问卷(自动评分)
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int submitAnswer(Long answerId);
|
||||
|
||||
/**
|
||||
* 生成问卷报告
|
||||
*
|
||||
* @param answerId 答题ID
|
||||
*/
|
||||
public void generateReport(Long answerId);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.ddnai.system.service.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireItem;
|
||||
|
||||
/**
|
||||
* 问卷题目表 服务层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface IPsyQuestionnaireItemService
|
||||
{
|
||||
/**
|
||||
* 查询题目信息
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 题目信息
|
||||
*/
|
||||
public PsyQuestionnaireItem selectItemById(Long itemId);
|
||||
|
||||
/**
|
||||
* 查询问卷题目列表
|
||||
*
|
||||
* @param questionnaireId 问卷ID
|
||||
* @return 题目集合
|
||||
*/
|
||||
public List<PsyQuestionnaireItem> selectItemListByQuestionnaireId(Long questionnaireId);
|
||||
|
||||
/**
|
||||
* 查询题目列表
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 题目集合
|
||||
*/
|
||||
public List<PsyQuestionnaireItem> selectItemList(PsyQuestionnaireItem item);
|
||||
|
||||
/**
|
||||
* 新增题目
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertItem(PsyQuestionnaireItem item);
|
||||
|
||||
/**
|
||||
* 修改题目
|
||||
*
|
||||
* @param item 题目信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateItem(PsyQuestionnaireItem item);
|
||||
|
||||
/**
|
||||
* 批量删除题目
|
||||
*
|
||||
* @param itemIds 需要删除的题目ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteItemByIds(Long[] itemIds);
|
||||
|
||||
/**
|
||||
* 删除题目信息
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteItemById(Long itemId);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.ddnai.system.service.psychology;
|
||||
|
||||
import java.util.List;
|
||||
import com.ddnai.system.domain.psychology.PsyQuestionnaireOption;
|
||||
|
||||
/**
|
||||
* 问卷选项表 服务层
|
||||
*
|
||||
* @author ddnai
|
||||
*/
|
||||
public interface IPsyQuestionnaireOptionService
|
||||
{
|
||||
/**
|
||||
* 查询选项信息
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 选项信息
|
||||
*/
|
||||
public PsyQuestionnaireOption selectOptionById(Long optionId);
|
||||
|
||||
/**
|
||||
* 查询题目选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 选项集合
|
||||
*/
|
||||
public List<PsyQuestionnaireOption> selectOptionListByItemId(Long itemId);
|
||||
|
||||
/**
|
||||
* 查询正确答案选项列表
|
||||
*
|
||||
* @param itemId 题目ID
|
||||
* @return 正确答案选项集合
|
||||
*/
|
||||
public List<PsyQuestionnaireOption> selectCorrectOptionListByItemId(Long itemId);
|
||||
|
||||
/**
|
||||
* 新增选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertOption(PsyQuestionnaireOption option);
|
||||
|
||||
/**
|
||||
* 修改选项
|
||||
*
|
||||
* @param option 选项信息
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateOption(PsyQuestionnaireOption option);
|
||||
|
||||
/**
|
||||
* 批量删除选项
|
||||
*
|
||||
* @param optionIds 需要删除的选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOptionByIds(Long[] optionIds);
|
||||
|
||||
/**
|
||||
* 删除选项信息
|
||||
*
|
||||
* @param optionId 选项ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOptionById(Long optionId);
|
||||
}
|
||||
|
||||
|
|
@ -85,5 +85,13 @@ public interface IPsyScaleService
|
|||
* @return 是否被使用
|
||||
*/
|
||||
public boolean isScaleInUse(Long scaleId);
|
||||
|
||||
/**
|
||||
* 导出量表数据(JSON格式)
|
||||
*
|
||||
* @param scaleIds 量表ID数组
|
||||
* @return 导出数据
|
||||
*/
|
||||
public List<com.ddnai.system.domain.psychology.vo.ScaleImportVO> exportScales(Long[] scaleIds);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,5 +118,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
select count(1) from psy_qrcode where qrcode_code = #{qrcodeCode}
|
||||
</select>
|
||||
|
||||
<select id="checkQrcodeCodeUniqueExcludeSelf" resultType="int">
|
||||
select count(1) from psy_qrcode
|
||||
where qrcode_code = #{qrcodeCode}
|
||||
<if test="qrcodeId != null">
|
||||
and qrcode_id != #{qrcodeId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ddnai.system.mapper.psychology.PsyQuestionnaireAnswerDetailMapper">
|
||||
|
||||
<resultMap type="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail" id="PsyQuestionnaireAnswerDetailResult">
|
||||
<result property="detailId" column="detail_id" />
|
||||
<result property="answerId" column="answer_id" />
|
||||
<result property="itemId" column="item_id" />
|
||||
<result property="optionId" column="option_id" />
|
||||
<result property="optionIds" column="option_ids" />
|
||||
<result property="answerText" column="answer_text" />
|
||||
<result property="answerScore" column="answer_score" />
|
||||
<result property="isSubjective" column="is_subjective" />
|
||||
<result property="isScored" column="is_scored" />
|
||||
<result property="scoredBy" column="scored_by" />
|
||||
<result property="scoredTime" column="scored_time" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectDetailVo">
|
||||
select detail_id, answer_id, item_id, option_id, option_ids, answer_text, answer_score,
|
||||
is_subjective, is_scored, scored_by, scored_time, create_time, update_time
|
||||
from psy_questionnaire_answer_detail
|
||||
</sql>
|
||||
|
||||
<select id="selectDetailById" parameterType="Long" resultMap="PsyQuestionnaireAnswerDetailResult">
|
||||
<include refid="selectDetailVo"/>
|
||||
where detail_id = #{detailId}
|
||||
</select>
|
||||
|
||||
<select id="selectDetailListByAnswerId" parameterType="Long" resultMap="PsyQuestionnaireAnswerDetailResult">
|
||||
<include refid="selectDetailVo"/>
|
||||
where answer_id = #{answerId}
|
||||
order by item_id
|
||||
</select>
|
||||
|
||||
<select id="selectDetailByAnswerIdAndItemId" resultMap="PsyQuestionnaireAnswerDetailResult">
|
||||
<include refid="selectDetailVo"/>
|
||||
where answer_id = #{answerId} and item_id = #{itemId}
|
||||
</select>
|
||||
|
||||
<insert id="insertDetail" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail" useGeneratedKeys="true" keyProperty="detailId">
|
||||
insert into psy_questionnaire_answer_detail (
|
||||
<if test="answerId != null">answer_id, </if>
|
||||
<if test="itemId != null">item_id, </if>
|
||||
<if test="optionId != null">option_id, </if>
|
||||
<if test="optionIds != null and optionIds != ''">option_ids, </if>
|
||||
<if test="answerText != null">answer_text, </if>
|
||||
<if test="answerScore != null">answer_score, </if>
|
||||
<if test="isSubjective != null and isSubjective != ''">is_subjective, </if>
|
||||
<if test="isScored != null and isScored != ''">is_scored, </if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="answerId != null">#{answerId}, </if>
|
||||
<if test="itemId != null">#{itemId}, </if>
|
||||
<if test="optionId != null">#{optionId}, </if>
|
||||
<if test="optionIds != null and optionIds != ''">#{optionIds}, </if>
|
||||
<if test="answerText != null">#{answerText}, </if>
|
||||
<if test="answerScore != null">#{answerScore}, </if>
|
||||
<if test="isSubjective != null and isSubjective != ''">#{isSubjective}, </if>
|
||||
<if test="isScored != null and isScored != ''">#{isScored}, </if>
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateDetail" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswerDetail">
|
||||
update psy_questionnaire_answer_detail
|
||||
<set>
|
||||
<if test="optionId != null">option_id = #{optionId}, </if>
|
||||
<if test="optionIds != null">option_ids = #{optionIds}, </if>
|
||||
<if test="answerText != null">answer_text = #{answerText}, </if>
|
||||
<if test="answerScore != null">answer_score = #{answerScore}, </if>
|
||||
<if test="isSubjective != null and isSubjective != ''">is_subjective = #{isSubjective}, </if>
|
||||
<if test="isScored != null and isScored != ''">is_scored = #{isScored}, </if>
|
||||
<if test="scoredBy != null and scoredBy != ''">scored_by = #{scoredBy}, </if>
|
||||
<if test="scoredTime != null">scored_time = #{scoredTime}, </if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where detail_id = #{detailId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteDetailById" parameterType="Long">
|
||||
delete from psy_questionnaire_answer_detail where detail_id = #{detailId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteDetailByIds" parameterType="String">
|
||||
delete from psy_questionnaire_answer_detail where detail_id in
|
||||
<foreach item="detailId" collection="array" open="(" separator="," close=")">
|
||||
#{detailId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ddnai.system.mapper.psychology.PsyQuestionnaireAnswerMapper">
|
||||
|
||||
<resultMap type="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer" id="PsyQuestionnaireAnswerResult">
|
||||
<result property="answerId" column="answer_id" />
|
||||
<result property="questionnaireId" column="questionnaire_id" />
|
||||
<result property="userId" column="user_id" />
|
||||
<result property="respondentName" column="respondent_name" />
|
||||
<result property="startTime" column="start_time" />
|
||||
<result property="submitTime" column="submit_time" />
|
||||
<result property="totalScore" column="total_score" />
|
||||
<result property="isPass" column="is_pass" />
|
||||
<result property="rank" column="rank" />
|
||||
<result property="status" column="status" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectAnswerVo">
|
||||
select answer_id, questionnaire_id, user_id, respondent_name, start_time, submit_time,
|
||||
total_score, is_pass, `rank`, `status`, create_by, create_time, update_by, update_time
|
||||
from psy_questionnaire_answer
|
||||
</sql>
|
||||
|
||||
<select id="selectAnswerById" parameterType="Long" resultMap="PsyQuestionnaireAnswerResult">
|
||||
<include refid="selectAnswerVo"/>
|
||||
where answer_id = #{answerId}
|
||||
</select>
|
||||
|
||||
<select id="selectAnswerList" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer" resultMap="PsyQuestionnaireAnswerResult">
|
||||
<include refid="selectAnswerVo"/>
|
||||
<where>
|
||||
<if test="questionnaireId != null"> and questionnaire_id = #{questionnaireId}</if>
|
||||
<if test="userId != null"> and user_id = #{userId}</if>
|
||||
<if test="status != null and status != ''"> and `status` = #{status}</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<insert id="insertAnswer" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer" useGeneratedKeys="true" keyProperty="answerId">
|
||||
insert into psy_questionnaire_answer (
|
||||
<if test="questionnaireId != null">questionnaire_id, </if>
|
||||
<if test="userId != null">user_id, </if>
|
||||
<if test="respondentName != null and respondentName != ''">respondent_name, </if>
|
||||
<if test="startTime != null">start_time, </if>
|
||||
<if test="status != null and status != ''">`status`, </if>
|
||||
<if test="createBy != null and createBy != ''">create_by, </if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="questionnaireId != null">#{questionnaireId}, </if>
|
||||
<if test="userId != null">#{userId}, </if>
|
||||
<if test="respondentName != null and respondentName != ''">#{respondentName}, </if>
|
||||
<if test="startTime != null">#{startTime}, </if>
|
||||
<if test="status != null and status != ''">#{status}, </if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy}, </if>
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateAnswer" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireAnswer">
|
||||
update psy_questionnaire_answer
|
||||
<set>
|
||||
<if test="submitTime != null">submit_time = #{submitTime}, </if>
|
||||
<if test="totalScore != null">total_score = #{totalScore}, </if>
|
||||
<if test="isPass != null and isPass != ''">is_pass = #{isPass}, </if>
|
||||
<if test="rank != null">`rank` = #{rank}, </if>
|
||||
<if test="status != null and status != ''">`status` = #{status}, </if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy}, </if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where answer_id = #{answerId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteAnswerByIds" parameterType="String">
|
||||
delete from psy_questionnaire_answer where answer_id in
|
||||
<foreach item="answerId" collection="array" open="(" separator="," close=")">
|
||||
#{answerId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ddnai.system.mapper.psychology.PsyQuestionnaireOptionMapper">
|
||||
|
||||
<resultMap type="com.ddnai.system.domain.psychology.PsyQuestionnaireOption" id="PsyQuestionnaireOptionResult">
|
||||
<result property="optionId" column="option_id" />
|
||||
<result property="itemId" column="item_id" />
|
||||
<result property="optionCode" column="option_code" />
|
||||
<result property="optionContent" column="option_content" />
|
||||
<result property="optionScore" column="option_score" />
|
||||
<result property="isCorrect" column="is_correct" />
|
||||
<result property="sortOrder" column="sort_order" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectOptionVo">
|
||||
select option_id, item_id, option_code, option_content, option_score, is_correct,
|
||||
sort_order, create_by, create_time, update_by, update_time
|
||||
from psy_questionnaire_option
|
||||
</sql>
|
||||
|
||||
<select id="selectOptionById" parameterType="Long" resultMap="PsyQuestionnaireOptionResult">
|
||||
<include refid="selectOptionVo"/>
|
||||
where option_id = #{optionId}
|
||||
</select>
|
||||
|
||||
<select id="selectOptionListByItemId" parameterType="Long" resultMap="PsyQuestionnaireOptionResult">
|
||||
<include refid="selectOptionVo"/>
|
||||
where item_id = #{itemId}
|
||||
order by sort_order
|
||||
</select>
|
||||
|
||||
<select id="selectCorrectOptionListByItemId" parameterType="Long" resultMap="PsyQuestionnaireOptionResult">
|
||||
<include refid="selectOptionVo"/>
|
||||
where item_id = #{itemId} and is_correct = '1'
|
||||
order by sort_order
|
||||
</select>
|
||||
|
||||
<insert id="insertOption" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireOption" useGeneratedKeys="true" keyProperty="optionId">
|
||||
insert into psy_questionnaire_option (
|
||||
<if test="itemId != null">item_id, </if>
|
||||
<if test="optionCode != null and optionCode != ''">option_code, </if>
|
||||
<if test="optionContent != null and optionContent != ''">option_content, </if>
|
||||
<if test="optionScore != null">option_score, </if>
|
||||
<if test="isCorrect != null and isCorrect != ''">is_correct, </if>
|
||||
<if test="sortOrder != null">sort_order, </if>
|
||||
<if test="createBy != null and createBy != ''">create_by, </if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="itemId != null">#{itemId}, </if>
|
||||
<if test="optionCode != null and optionCode != ''">#{optionCode}, </if>
|
||||
<if test="optionContent != null and optionContent != ''">#{optionContent}, </if>
|
||||
<if test="optionScore != null">#{optionScore}, </if>
|
||||
<if test="isCorrect != null and isCorrect != ''">#{isCorrect}, </if>
|
||||
<if test="sortOrder != null">#{sortOrder}, </if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy}, </if>
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateOption" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireOption">
|
||||
update psy_questionnaire_option
|
||||
<set>
|
||||
<if test="optionCode != null and optionCode != ''">option_code = #{optionCode}, </if>
|
||||
<if test="optionContent != null and optionContent != ''">option_content = #{optionContent}, </if>
|
||||
<if test="optionScore != null">option_score = #{optionScore}, </if>
|
||||
<if test="isCorrect != null and isCorrect != ''">is_correct = #{isCorrect}, </if>
|
||||
<if test="sortOrder != null">sort_order = #{sortOrder}, </if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy}, </if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where option_id = #{optionId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteOptionById" parameterType="Long">
|
||||
delete from psy_questionnaire_option where option_id = #{optionId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteOptionByIds" parameterType="String">
|
||||
delete from psy_questionnaire_option where option_id in
|
||||
<foreach item="optionId" collection="array" open="(" separator="," close=")">
|
||||
#{optionId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteOptionByItemId" parameterType="Long">
|
||||
delete from psy_questionnaire_option where item_id = #{itemId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ddnai.system.mapper.psychology.PsyQuestionnaireReportMapper">
|
||||
|
||||
<resultMap type="com.ddnai.system.domain.psychology.PsyQuestionnaireReport" id="PsyQuestionnaireReportResult">
|
||||
<result property="reportId" column="report_id" />
|
||||
<result property="answerId" column="answer_id" />
|
||||
<result property="reportType" column="report_type" />
|
||||
<result property="reportTitle" column="report_title" />
|
||||
<result property="reportContent" column="report_content" />
|
||||
<result property="summary" column="summary" />
|
||||
<result property="chartData" column="chart_data" />
|
||||
<result property="pdfPath" column="pdf_path" />
|
||||
<result property="isGenerated" column="is_generated" />
|
||||
<result property="generateTime" column="generate_time" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectReportVo">
|
||||
select report_id, answer_id, report_type, report_title, report_content, summary,
|
||||
chart_data, pdf_path, is_generated, generate_time, create_by, create_time,
|
||||
update_by, update_time
|
||||
from psy_questionnaire_report
|
||||
</sql>
|
||||
|
||||
<select id="selectReportById" parameterType="Long" resultMap="PsyQuestionnaireReportResult">
|
||||
<include refid="selectReportVo"/>
|
||||
where report_id = #{reportId}
|
||||
</select>
|
||||
|
||||
<select id="selectReportByAnswerId" parameterType="Long" resultMap="PsyQuestionnaireReportResult">
|
||||
<include refid="selectReportVo"/>
|
||||
where answer_id = #{answerId}
|
||||
</select>
|
||||
|
||||
<select id="selectReportList" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireReport" resultMap="PsyQuestionnaireReportResult">
|
||||
<include refid="selectReportVo"/>
|
||||
<where>
|
||||
<if test="answerId != null"> and answer_id = #{answerId}</if>
|
||||
<if test="reportType != null and reportType != ''"> and report_type = #{reportType}</if>
|
||||
<if test="isGenerated != null and isGenerated != ''"> and is_generated = #{isGenerated}</if>
|
||||
</where>
|
||||
order by create_time desc
|
||||
</select>
|
||||
|
||||
<insert id="insertReport" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireReport" useGeneratedKeys="true" keyProperty="reportId">
|
||||
insert into psy_questionnaire_report (
|
||||
<if test="answerId != null">answer_id, </if>
|
||||
<if test="reportType != null and reportType != ''">report_type, </if>
|
||||
<if test="reportTitle != null and reportTitle != ''">report_title, </if>
|
||||
<if test="reportContent != null">report_content, </if>
|
||||
<if test="summary != null">summary, </if>
|
||||
<if test="chartData != null">chart_data, </if>
|
||||
<if test="pdfPath != null">pdf_path, </if>
|
||||
<if test="isGenerated != null and isGenerated != ''">is_generated, </if>
|
||||
<if test="generateTime != null">generate_time, </if>
|
||||
<if test="createBy != null and createBy != ''">create_by, </if>
|
||||
create_time
|
||||
)values(
|
||||
<if test="answerId != null">#{answerId}, </if>
|
||||
<if test="reportType != null and reportType != ''">#{reportType}, </if>
|
||||
<if test="reportTitle != null and reportTitle != ''">#{reportTitle}, </if>
|
||||
<if test="reportContent != null">#{reportContent}, </if>
|
||||
<if test="summary != null">#{summary}, </if>
|
||||
<if test="chartData != null">#{chartData}, </if>
|
||||
<if test="pdfPath != null">#{pdfPath}, </if>
|
||||
<if test="isGenerated != null and isGenerated != ''">#{isGenerated}, </if>
|
||||
<if test="generateTime != null">#{generateTime}, </if>
|
||||
<if test="createBy != null and createBy != ''">#{createBy}, </if>
|
||||
sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateReport" parameterType="com.ddnai.system.domain.psychology.PsyQuestionnaireReport">
|
||||
update psy_questionnaire_report
|
||||
<set>
|
||||
<if test="reportType != null and reportType != ''">report_type = #{reportType}, </if>
|
||||
<if test="reportTitle != null and reportTitle != ''">report_title = #{reportTitle}, </if>
|
||||
<if test="reportContent != null">report_content = #{reportContent}, </if>
|
||||
<if test="summary != null">summary = #{summary}, </if>
|
||||
<if test="chartData != null">chart_data = #{chartData}, </if>
|
||||
<if test="pdfPath != null">pdf_path = #{pdfPath}, </if>
|
||||
<if test="isGenerated != null and isGenerated != ''">is_generated = #{isGenerated}, </if>
|
||||
<if test="generateTime != null">generate_time = #{generateTime}, </if>
|
||||
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy}, </if>
|
||||
update_time = sysdate()
|
||||
</set>
|
||||
where report_id = #{reportId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteReportById" parameterType="Long">
|
||||
delete from psy_questionnaire_report where report_id = #{reportId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteReportByIds" parameterType="String">
|
||||
delete from psy_questionnaire_report where report_id in
|
||||
<foreach item="reportId" collection="array" open="(" separator="," close=")">
|
||||
#{reportId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
-- ========================================
|
||||
-- 检查重复菜单查询脚本
|
||||
-- 用途:在执行清理前查看数据库中的重复菜单
|
||||
-- ========================================
|
||||
USE ry_news;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========================================
|
||||
-- 1. 查找所有重复的菜单(基于menu_name和path组合)
|
||||
-- ========================================
|
||||
SELECT
|
||||
menu_name AS '菜单名称',
|
||||
path AS '路由路径',
|
||||
component AS '组件路径',
|
||||
parent_id AS '父菜单ID',
|
||||
COUNT(*) AS '重复数量',
|
||||
GROUP_CONCAT(menu_id ORDER BY menu_id SEPARATOR ', ') AS '菜单ID列表'
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
GROUP BY menu_name, path, component, parent_id
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY COUNT(*) DESC, menu_name;
|
||||
|
||||
-- ========================================
|
||||
-- 2. 统计心理学相关菜单总数
|
||||
-- ========================================
|
||||
SELECT
|
||||
'心理学相关菜单总数' AS '统计项',
|
||||
COUNT(*) AS '数量'
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%';
|
||||
|
||||
-- ========================================
|
||||
-- 3. 查找重复的父菜单(目录)
|
||||
-- ========================================
|
||||
SELECT
|
||||
menu_name AS '目录名称',
|
||||
COUNT(*) AS '重复数量',
|
||||
GROUP_CONCAT(menu_id ORDER BY menu_id SEPARATOR ', ') AS '菜单ID列表'
|
||||
FROM sys_menu
|
||||
WHERE parent_id = 0
|
||||
AND (menu_name LIKE '%心理%' OR menu_name LIKE '%网站%')
|
||||
GROUP BY menu_name
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- ========================================
|
||||
-- 4. 列出所有心理学相关菜单(按层级)
|
||||
-- ========================================
|
||||
SELECT
|
||||
menu_id,
|
||||
menu_name AS '菜单名称',
|
||||
parent_id AS '父菜单ID',
|
||||
path AS '路由路径',
|
||||
component AS '组件路径',
|
||||
menu_type AS '类型',
|
||||
visible AS '是否显示',
|
||||
order_num AS '排序'
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
ORDER BY parent_id, order_num, menu_id;
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
-- ========================================
|
||||
-- 清理重复菜单SQL脚本
|
||||
-- 用途:删除数据库中重复的心理学相关菜单
|
||||
-- ========================================
|
||||
USE ry_news;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========================================
|
||||
-- 1. 查找重复的菜单(基于menu_name和path组合)
|
||||
-- ========================================
|
||||
-- 先查看重复的菜单
|
||||
SELECT menu_name, path, component, COUNT(*) as count
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
GROUP BY menu_name, path, component
|
||||
HAVING count > 1;
|
||||
|
||||
-- ========================================
|
||||
-- 2. 删除重复的菜单(保留menu_id最小的那个)
|
||||
-- ========================================
|
||||
-- 删除"心理测评管理"目录的重复项(保留第一个)
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = '心理测评管理'
|
||||
AND t1.parent_id = 0
|
||||
AND t2.menu_name = '心理测评管理'
|
||||
AND t2.parent_id = 0
|
||||
AND t1.menu_id > t2.menu_id;
|
||||
|
||||
-- 删除"心理网站管理"目录的重复项
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = '心理网站管理'
|
||||
AND t1.parent_id = 0
|
||||
AND t2.menu_name = '心理网站管理'
|
||||
AND t2.parent_id = 0
|
||||
AND t1.menu_id > t2.menu_id;
|
||||
|
||||
-- 删除基于menu_name和parent_id的重复菜单(优先处理)
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = t2.menu_name
|
||||
AND t1.parent_id = t2.parent_id
|
||||
AND t1.menu_id > t2.menu_id
|
||||
AND (t1.menu_name LIKE '%心理%'
|
||||
OR t1.menu_name LIKE '%量表%'
|
||||
OR t1.menu_name LIKE '%题目%'
|
||||
OR t1.menu_name LIKE '%因子%'
|
||||
OR t1.menu_name LIKE '%测评%'
|
||||
OR t1.menu_name LIKE '%报告%'
|
||||
OR t1.menu_name LIKE '%解释%'
|
||||
OR t1.menu_name LIKE '%档案%'
|
||||
OR t1.menu_name LIKE '%问卷%'
|
||||
OR t1.menu_name LIKE '%网站%'
|
||||
OR t1.menu_name LIKE '%栏目%'
|
||||
OR t1.menu_name LIKE '%评论%'
|
||||
OR t1.menu_name LIKE '%预警%'
|
||||
OR t1.menu_name LIKE '%规则%');
|
||||
|
||||
-- 删除其他重复菜单(基于path和component)
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.path = t2.path
|
||||
AND (t1.component = t2.component OR (t1.component IS NULL AND t2.component IS NULL))
|
||||
AND t1.menu_name = t2.menu_name
|
||||
AND t1.parent_id = t2.parent_id
|
||||
AND t1.menu_id > t2.menu_id
|
||||
AND (t1.menu_name LIKE '%心理%'
|
||||
OR t1.menu_name LIKE '%量表%'
|
||||
OR t1.menu_name LIKE '%题目%'
|
||||
OR t1.menu_name LIKE '%因子%'
|
||||
OR t1.menu_name LIKE '%测评%'
|
||||
OR t1.menu_name LIKE '%报告%'
|
||||
OR t1.menu_name LIKE '%解释%'
|
||||
OR t1.menu_name LIKE '%档案%'
|
||||
OR t1.menu_name LIKE '%问卷%'
|
||||
OR t1.menu_name LIKE '%网站%'
|
||||
OR t1.menu_name LIKE '%栏目%'
|
||||
OR t1.menu_name LIKE '%评论%'
|
||||
OR t1.menu_name LIKE '%预警%'
|
||||
OR t1.menu_name LIKE '%规则%');
|
||||
|
||||
-- ========================================
|
||||
-- 3. 清理孤立的子菜单(父菜单已被删除)
|
||||
-- ========================================
|
||||
-- 删除那些父菜单ID不存在于sys_menu表中的子菜单
|
||||
DELETE FROM sys_menu
|
||||
WHERE parent_id > 0
|
||||
AND parent_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp)
|
||||
AND (menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
OR menu_name LIKE '%规则%');
|
||||
|
||||
-- ========================================
|
||||
-- 4. 清理角色菜单关联表中的孤立记录
|
||||
-- ========================================
|
||||
-- 删除指向已删除菜单的角色菜单关联
|
||||
DELETE FROM sys_role_menu
|
||||
WHERE menu_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp2);
|
||||
|
||||
-- ========================================
|
||||
-- 5. 验证清理结果
|
||||
-- ========================================
|
||||
SELECT '清理完成!' AS result;
|
||||
|
||||
-- 检查是否还有重复菜单
|
||||
SELECT
|
||||
menu_name AS '菜单名称',
|
||||
path AS '路由路径',
|
||||
component AS '组件路径',
|
||||
parent_id AS '父菜单ID',
|
||||
COUNT(*) AS '剩余数量'
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
OR menu_name LIKE '%规则%'
|
||||
GROUP BY menu_name, path, component, parent_id
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 如果没有输出,说明没有重复菜单了
|
||||
SELECT '如果上面的查询没有返回结果,说明所有重复菜单已清理完成!' AS message;
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
-- ========================================
|
||||
-- 增强版清理重复菜单SQL脚本
|
||||
-- 用途:彻底删除数据库中重复的心理学相关菜单
|
||||
-- 注意:执行前请备份数据库!
|
||||
-- ========================================
|
||||
USE ry_news;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========================================
|
||||
-- 第一步:删除重复的父菜单(目录)
|
||||
-- ========================================
|
||||
-- 删除重复的"心理测评管理"目录(保留menu_id最小的)
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = '心理测评管理'
|
||||
AND t1.parent_id = 0
|
||||
AND t2.menu_name = '心理测评管理'
|
||||
AND t2.parent_id = 0
|
||||
AND t1.menu_id > t2.menu_id;
|
||||
|
||||
-- 删除重复的"心理网站管理"目录
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = '心理网站管理'
|
||||
AND t1.parent_id = 0
|
||||
AND t2.menu_name = '心理网站管理'
|
||||
AND t2.parent_id = 0
|
||||
AND t1.menu_id > t2.menu_id;
|
||||
|
||||
-- ========================================
|
||||
-- 第二步:删除基于menu_name和parent_id的重复菜单
|
||||
-- ========================================
|
||||
-- 对于相同名称和父菜单的重复项,保留最小的menu_id
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.menu_name = t2.menu_name
|
||||
AND t1.parent_id = t2.parent_id
|
||||
AND t1.menu_id > t2.menu_id
|
||||
AND (t1.menu_name LIKE '%心理%'
|
||||
OR t1.menu_name LIKE '%量表%'
|
||||
OR t1.menu_name LIKE '%题目%'
|
||||
OR t1.menu_name LIKE '%因子%'
|
||||
OR t1.menu_name LIKE '%测评%'
|
||||
OR t1.menu_name LIKE '%报告%'
|
||||
OR t1.menu_name LIKE '%解释%'
|
||||
OR t1.menu_name LIKE '%档案%'
|
||||
OR t1.menu_name LIKE '%问卷%'
|
||||
OR t1.menu_name LIKE '%网站%'
|
||||
OR t1.menu_name LIKE '%栏目%'
|
||||
OR t1.menu_name LIKE '%评论%'
|
||||
OR t1.menu_name LIKE '%预警%'
|
||||
OR t1.menu_name LIKE '%规则%');
|
||||
|
||||
-- ========================================
|
||||
-- 第三步:删除基于path和component的重复菜单
|
||||
-- ========================================
|
||||
-- 删除所有重复菜单(保留menu_id最小的)
|
||||
DELETE t1 FROM sys_menu t1
|
||||
INNER JOIN sys_menu t2
|
||||
WHERE t1.path = t2.path
|
||||
AND (t1.component = t2.component OR (t1.component IS NULL AND t2.component IS NULL))
|
||||
AND t1.menu_name = t2.menu_name
|
||||
AND t1.parent_id = t2.parent_id
|
||||
AND t1.menu_id > t2.menu_id
|
||||
AND (t1.menu_name LIKE '%心理%'
|
||||
OR t1.menu_name LIKE '%量表%'
|
||||
OR t1.menu_name LIKE '%题目%'
|
||||
OR t1.menu_name LIKE '%因子%'
|
||||
OR t1.menu_name LIKE '%测评%'
|
||||
OR t1.menu_name LIKE '%报告%'
|
||||
OR t1.menu_name LIKE '%解释%'
|
||||
OR t1.menu_name LIKE '%档案%'
|
||||
OR t1.menu_name LIKE '%问卷%'
|
||||
OR t1.menu_name LIKE '%网站%'
|
||||
OR t1.menu_name LIKE '%栏目%'
|
||||
OR t1.menu_name LIKE '%评论%'
|
||||
OR t1.menu_name LIKE '%预警%'
|
||||
OR t1.menu_name LIKE '%规则%');
|
||||
|
||||
-- ========================================
|
||||
-- 第四步:清理孤立的子菜单(父菜单已被删除)
|
||||
-- ========================================
|
||||
-- 删除那些父菜单ID不存在于sys_menu表中的子菜单
|
||||
DELETE FROM sys_menu
|
||||
WHERE parent_id > 0
|
||||
AND parent_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp)
|
||||
AND (menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
OR menu_name LIKE '%规则%');
|
||||
|
||||
-- ========================================
|
||||
-- 第五步:清理角色菜单关联表中的孤立记录
|
||||
-- ========================================
|
||||
-- 删除指向已删除菜单的角色菜单关联
|
||||
DELETE FROM sys_role_menu
|
||||
WHERE menu_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp2);
|
||||
|
||||
-- ========================================
|
||||
-- 第六步:验证清理结果
|
||||
-- ========================================
|
||||
SELECT '清理完成!' AS result;
|
||||
|
||||
-- 检查是否还有重复菜单
|
||||
SELECT
|
||||
menu_name AS '菜单名称',
|
||||
path AS '路由路径',
|
||||
component AS '组件路径',
|
||||
parent_id AS '父菜单ID',
|
||||
COUNT(*) AS '剩余数量'
|
||||
FROM sys_menu
|
||||
WHERE menu_name LIKE '%心理%'
|
||||
OR menu_name LIKE '%量表%'
|
||||
OR menu_name LIKE '%题目%'
|
||||
OR menu_name LIKE '%因子%'
|
||||
OR menu_name LIKE '%测评%'
|
||||
OR menu_name LIKE '%报告%'
|
||||
OR menu_name LIKE '%解释%'
|
||||
OR menu_name LIKE '%档案%'
|
||||
OR menu_name LIKE '%问卷%'
|
||||
OR menu_name LIKE '%网站%'
|
||||
OR menu_name LIKE '%栏目%'
|
||||
OR menu_name LIKE '%评论%'
|
||||
OR menu_name LIKE '%预警%'
|
||||
OR menu_name LIKE '%规则%'
|
||||
GROUP BY menu_name, path, component, parent_id
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 如果没有输出,说明没有重复菜单了
|
||||
SELECT '如果上面的查询没有返回结果,说明所有重复菜单已清理完成!' AS message;
|
||||
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
-- ========================================
|
||||
-- 启用用户注册功能和添加量表权限管理菜单
|
||||
-- ========================================
|
||||
USE ry_news;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========================================
|
||||
-- 1. 启用用户注册功能
|
||||
-- ========================================
|
||||
-- 检查并更新注册功能配置
|
||||
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
|
||||
VALUES ('用户注册开关', 'sys.account.registerUser', 'true', 'Y', 'admin', NOW(), '是否开启用户注册功能(true开启 false关闭)')
|
||||
ON DUPLICATE KEY UPDATE config_value = 'true', update_by = 'admin', update_time = NOW();
|
||||
|
||||
-- ========================================
|
||||
-- 2. 量表权限管理菜单配置
|
||||
-- ========================================
|
||||
-- 量表权限管理主菜单
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '量表权限管理', menu_id, 11, 'permission', 'psychology/permission/index', 1, 0, 'C', '0', '0', 'psychology:permission:list', 'lock', 'admin', NOW(), '量表权限管理菜单'
|
||||
FROM sys_menu WHERE menu_name = '心理测评管理' AND parent_id = 0 LIMIT 1;
|
||||
|
||||
-- 量表权限管理按钮权限
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限查询', menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:query', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限新增', menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:add', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限修改', menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:edit', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限删除', menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:remove', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
-- ========================================
|
||||
-- 3. 为管理员角色分配量表权限管理菜单权限
|
||||
-- ========================================
|
||||
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu_id FROM sys_menu
|
||||
WHERE (menu_name = '量表权限管理'
|
||||
OR menu_name LIKE '%权限查询%'
|
||||
OR menu_name LIKE '%权限新增%'
|
||||
OR menu_name LIKE '%权限修改%'
|
||||
OR menu_name LIKE '%权限删除%')
|
||||
AND menu_name LIKE '%权限%';
|
||||
|
||||
-- ========================================
|
||||
-- 验证配置
|
||||
-- ========================================
|
||||
SELECT '用户注册功能和量表权限管理菜单配置完成!' AS result;
|
||||
SELECT config_value AS register_enabled FROM sys_config WHERE config_key = 'sys.account.registerUser';
|
||||
SELECT COUNT(*) AS permission_menu_count FROM sys_menu WHERE menu_name LIKE '%量表权限%' OR menu_name LIKE '%权限%';
|
||||
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
-- ========================================
|
||||
-- 量表权限管理菜单配置
|
||||
-- ========================================
|
||||
USE ry_news;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========================================
|
||||
-- 量表权限管理
|
||||
-- ========================================
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '量表权限管理', menu_id, 11, 'permission', 'psychology/permission/index', 1, 0, 'C', '0', '0', 'psychology:permission:list', 'lock', 'admin', NOW(), '量表权限管理菜单'
|
||||
FROM sys_menu WHERE menu_name = '心理测评管理' AND parent_id = 0 LIMIT 1;
|
||||
|
||||
-- 量表权限管理按钮权限
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限查询', menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:query', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限新增', menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:add', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限修改', menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:edit', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
|
||||
SELECT '权限删除', menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:remove', '#', 'admin', NOW(), ''
|
||||
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
|
||||
|
||||
-- ========================================
|
||||
-- 为管理员角色分配权限
|
||||
-- ========================================
|
||||
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '量表权限管理' OR menu_name LIKE '%权限查询%' OR menu_name LIKE '%权限新增%' OR menu_name LIKE '%权限修改%' OR menu_name LIKE '%权限删除%';
|
||||
|
||||
SELECT '量表权限管理菜单配置完成!' AS result;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2189
sql/心理量表.sql
Normal file
2189
sql/心理量表.sql
Normal file
File diff suppressed because one or more lines are too long
290
z-Trae/分析缺失部分.md
290
z-Trae/分析缺失部分.md
|
|
@ -1,290 +0,0 @@
|
|||
|
||||
|
||||
|
||||
|
||||
## 1. 题目管理和因子管理路由缺失问题
|
||||
|
||||
### 问题确认
|
||||
经过详细检查,题目管理和因子管理页面出现404错误的根本原因是**前端路由配置缺失**:
|
||||
|
||||
1. **组件文件存在**:
|
||||
- 题目管理组件:`ruoyi-ui/src/views/psychology/scale/item.vue` 已完整实现
|
||||
- 因子管理组件:`ruoyi-ui/src/views/psychology/scale/factor.vue` 已完整实现
|
||||
|
||||
2. **API接口完整**:
|
||||
- 题目管理API:`ruoyi-ui/src/api/psychology/item.js`
|
||||
- 因子管理API:`ruoyi-ui/src/api/psychology/factor.js`
|
||||
- 选项管理API:`ruoyi-ui/src/api/psychology/option.js`
|
||||
|
||||
3. **跳转逻辑正确**:
|
||||
- 在 `index.vue` 中第407-414行,通过 `$router.push` 分别跳转到 `/psychology/scale/item` 和 `/psychology/scale/factor`
|
||||
|
||||
4. **路由配置缺失**:
|
||||
- `router/index.js` 中未定义这两个关键路径的路由规则
|
||||
|
||||
### 解决方案
|
||||
需要在 `router/index.js` 的动态路由配置中添加以下代码:
|
||||
|
||||
```javascript
|
||||
// 在dynamicRoutes数组中添加
|
||||
{
|
||||
path: '/psychology',
|
||||
component: Layout,
|
||||
redirect: '/psychology/scale',
|
||||
name: 'Psychology',
|
||||
meta: { title: '心理测评', icon: 'psychology' },
|
||||
children: [
|
||||
{
|
||||
path: 'scale',
|
||||
component: () => import('@/views/psychology/scale/index'),
|
||||
name: 'PsyScale',
|
||||
meta: { title: '量表管理', icon: 'scale' }
|
||||
},
|
||||
{
|
||||
path: 'scale/item',
|
||||
component: () => import('@/views/psychology/scale/item'),
|
||||
name: 'PsyItem',
|
||||
meta: { title: '题目管理', icon: 'item', activeMenu: '/psychology/scale' }
|
||||
},
|
||||
{
|
||||
path: 'scale/factor',
|
||||
component: () => import('@/views/psychology/scale/factor'),
|
||||
name: 'PsyFactor',
|
||||
meta: { title: '因子管理', icon: 'factor', activeMenu: '/psychology/scale' }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 测评页面无量表问题分析
|
||||
|
||||
根据代码分析,测评页面无量表的可能原因包括:
|
||||
|
||||
1. **菜单配置不完整**:
|
||||
- 需执行 `Psychological.sql` 中的菜单配置SQL
|
||||
- 确保管理员账户拥有相关权限
|
||||
|
||||
2. **数据库初始化问题**:
|
||||
- 检查 `psy_scale` 表是否有初始化数据
|
||||
- 验证量表状态是否设置为启用(status=0)
|
||||
|
||||
3. **前端缓存问题**:
|
||||
- 清除浏览器缓存和Vue路由缓存
|
||||
- 重新登录系统刷新权限
|
||||
|
||||
## 3. 其他潜在问题
|
||||
|
||||
1. **权限配置**:
|
||||
- 确保相关权限码 `psychology:item:list` 和 `psychology:factor:list` 正确配置
|
||||
|
||||
2. **组件交互**:
|
||||
- 题目管理和因子管理页面需要通过URL参数 `scaleId` 和 `scaleName` 传递数据
|
||||
|
||||
## 4. 其他功能模块路由检查
|
||||
|
||||
通过进一步分析,系统还包含以下心理测评相关功能模块,这些模块的路由也可能存在缺失问题:
|
||||
|
||||
### 4.1 测评管理模块
|
||||
- 测评列表页面:`ruoyi-ui/src/views/psychology/assessment/index.vue`
|
||||
- 开始测评页面:`ruoyi-ui/src/views/psychology/assessment/start.vue`
|
||||
- 答题页面:`ruoyi-ui/src/views/psychology/assessment/taking.vue`
|
||||
- 测评报告页面(疑似存在引用)
|
||||
|
||||
### 4.2 自定义问卷模块
|
||||
- 问卷管理页面:`ruoyi-ui/src/views/psychology/questionnaire/index.vue`
|
||||
|
||||
### 4.3 结果解释模块
|
||||
- 结果解释管理页面:`ruoyi-ui/src/views/psychology/interpretation/index.vue`
|
||||
|
||||
## 5. 完整路由配置建议
|
||||
|
||||
建议在`router/index.js`中添加以下完整的路由配置,以确保所有功能模块正常访问:
|
||||
|
||||
```javascript
|
||||
// 在dynamicRoutes数组中添加完整的心理测评模块路由
|
||||
{
|
||||
path: '/psychology',
|
||||
component: Layout,
|
||||
redirect: '/psychology/scale',
|
||||
name: 'Psychology',
|
||||
meta: { title: '心理测评', icon: 'psychology' },
|
||||
children: [
|
||||
// 量表管理
|
||||
{
|
||||
path: 'scale',
|
||||
component: () => import('@/views/psychology/scale/index'),
|
||||
name: 'PsyScale',
|
||||
meta: { title: '量表管理', icon: 'scale' }
|
||||
},
|
||||
{
|
||||
path: 'scale/item',
|
||||
component: () => import('@/views/psychology/scale/item'),
|
||||
name: 'PsyItem',
|
||||
meta: { title: '题目管理', icon: 'item', activeMenu: '/psychology/scale' }
|
||||
},
|
||||
{
|
||||
path: 'scale/factor',
|
||||
component: () => import('@/views/psychology/scale/factor'),
|
||||
name: 'PsyFactor',
|
||||
meta: { title: '因子管理', icon: 'factor', activeMenu: '/psychology/scale' }
|
||||
},
|
||||
// 测评管理
|
||||
{
|
||||
path: 'assessment',
|
||||
component: () => import('@/views/psychology/assessment/index'),
|
||||
name: 'PsyAssessment',
|
||||
meta: { title: '测评管理', icon: 'assessment' }
|
||||
},
|
||||
{
|
||||
path: 'assessment/start',
|
||||
component: () => import('@/views/psychology/assessment/start'),
|
||||
name: 'PsyAssessmentStart',
|
||||
meta: { title: '开始测评', icon: 'start', activeMenu: '/psychology/assessment' }
|
||||
},
|
||||
{
|
||||
path: 'assessment/taking',
|
||||
component: () => import('@/views/psychology/assessment/taking'),
|
||||
name: 'PsyAssessmentTaking',
|
||||
meta: { title: '正在测评', icon: 'taking', activeMenu: '/psychology/assessment' }
|
||||
},
|
||||
{
|
||||
path: 'assessment/report',
|
||||
component: () => import('@/views/psychology/assessment/report'),
|
||||
name: 'PsyAssessmentReport',
|
||||
meta: { title: '测评报告', icon: 'report', activeMenu: '/psychology/assessment' }
|
||||
},
|
||||
// 自定义问卷
|
||||
{
|
||||
path: 'questionnaire',
|
||||
component: () => import('@/views/psychology/questionnaire/index'),
|
||||
name: 'PsyQuestionnaire',
|
||||
meta: { title: '问卷管理', icon: 'questionnaire' }
|
||||
},
|
||||
// 结果解释
|
||||
{
|
||||
path: 'interpretation',
|
||||
component: () => import('@/views/psychology/interpretation/index'),
|
||||
name: 'PsyInterpretation',
|
||||
meta: { title: '结果解释', icon: 'interpretation' }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 总结
|
||||
|
||||
心理测评系统的核心功能组件和API接口都已完整实现,但存在严重的前端路由配置缺失问题。主要发现:
|
||||
|
||||
1. **题目管理和因子管理页面404错误**:组件已实现但路由未配置
|
||||
2. **其他功能模块也可能存在类似问题**:测评管理、问卷管理、结果解释等模块的路由也需要检查配置
|
||||
3. **功能依赖关系明确**:组件间通过路由跳转和参数传递正常工作
|
||||
|
||||
解决方案:
|
||||
1. 添加完整的路由配置到`router/index.js`
|
||||
2. 执行`Psychological.sql`中的菜单配置SQL确保菜单显示
|
||||
3. 检查用户权限配置确保功能访问权限
|
||||
4. 清除浏览器缓存并重新登录系统
|
||||
|
||||
通过这些修改,心理测评系统的所有功能应该能够正常访问和使用。
|
||||
要对比系统与需求文档的差别,首先需要了解当前系统的实际实现情况。让我搜索系统的核心功能实现信息。
|
||||
|
||||
|
||||
toolName: search_codebase
|
||||
|
||||
status: success
|
||||
|
||||
query: 心理测评系统 核心功能实现 量表管理 测评执行 报告生成
|
||||
|
||||
我需要查看更多关于项目开发状态的信息,特别是具体的完成情况和未完成功能。让我查看开发任务清单和进度总结的完整内容。
|
||||
|
||||
|
||||
toolName: view_files
|
||||
|
||||
status: success
|
||||
|
||||
|
||||
filePath: d:\0_Project\01_Project_psychological\project\RuoYi-Vue-redis\z_Project change\2-开发任务清单.md
|
||||
d:\0_Project\01_Project_psychological\project\RuoYi-Vue-redis\z_Project change\5-第二阶段全面完成总结.md
|
||||
|
||||
让我查看功能测试说明文档,以了解系统的实际功能实现细节,这有助于更准确地对比系统与需求文档的差异。
|
||||
|
||||
|
||||
toolName: view_files
|
||||
|
||||
status: success
|
||||
|
||||
|
||||
filePath: d:\0_Project\01_Project_psychological\project\RuoYi-Vue-redis\z_Project change\使用指南-功能测试说明.md
|
||||
|
||||
# 心理测评系统实现与需求对比分析
|
||||
|
||||
## 已完成功能对比
|
||||
|
||||
### 1. 核心测评流程 ✅
|
||||
- ✅ **测评暂停/恢复功能**:已完整实现,用户可随时暂停并在下次继续测评
|
||||
- ✅ **量表管理**:支持管理员自行添加定义所需量表,设置属性和分类
|
||||
- ✅ **报告生成**:支持测评结果自动生成,管理员可修改解释内容
|
||||
- ✅ **用户档案功能**:已实现用户档案管理,支持自定义字段配置
|
||||
|
||||
### 2. 数据结构与基础功能 ✅
|
||||
- ✅ **数据库设计**:创建了19个核心表,包括量表、题目、选项、因子等
|
||||
- ✅ **管理员权限**:已实现基本的权限控制和管理功能
|
||||
- ✅ **测评执行模块**:完整实现开始测评、答题、提交等核心流程
|
||||
|
||||
## 部分完成功能
|
||||
|
||||
### 1. 自定义问卷 ⚠️
|
||||
- ✅ **基础功能**:实现了问卷的基本CRUD操作
|
||||
- ❌ **多种题目类型**:虽然预留了接口,但完整的8种题目类型支持尚未实现
|
||||
- ❌ **自动计分和排名统计**:客观题自动打分和成绩排名功能未实现
|
||||
|
||||
### 2. 二维码功能 ⚠️
|
||||
- ✅ **基础功能**:实现了二维码生成和展示
|
||||
- ❌ **扫码功能**:移动端扫码页面适配未完成
|
||||
- ❌ **完整的扫码流程**:扫码测试、查看报告、注册登录等完整流程未实现
|
||||
|
||||
### 3. 心理网站功能 ⚠️
|
||||
- ✅ **后台管理**:网站内容管理功能已在后台实现
|
||||
- ❌ **前台展示**:网站前台展示页面尚未开发
|
||||
|
||||
## 未实现功能
|
||||
|
||||
### 1. 核心需求标记功能 ❌
|
||||
- ❌ **语音真人读题功能**:未实现(需求文档标★功能)
|
||||
- ❌ **自动危机预警功能**:未实现自动危机预警及自动解除预警(需求文档标★功能)
|
||||
|
||||
### 2. 数据管理功能 ❌
|
||||
- ❌ **批量导入测评结果**:未实现Excel批量导入功能
|
||||
- ❌ **数据一键备份和还原**:未实现自动定期备份功能
|
||||
- ❌ **多种形式的数据录入**:完整的Excel导入、批量生成等功能未实现
|
||||
|
||||
### 3. 系统集成功能 ❌
|
||||
- ❌ **开放式API接口**:专用API数据对接端口未实现
|
||||
- ❌ **单机/网络模式切换**:支持U盘运行等绿色架构功能未实现
|
||||
|
||||
### 4. 量表数据 ❌
|
||||
- ✅ **COPA-SAI**:已录入示例数据
|
||||
- ❌ **其他COPA系列量表**:COPA-PI、COPA-TCMI等6个量表仅预留接口,需用户导入
|
||||
- ❌ **经典心理量表**:50+经典量表数据未导入
|
||||
|
||||
### 5. 高级功能 ❌
|
||||
- ❌ **数据统计分析**:团体和个体数据统计功能未实现
|
||||
- ❌ **测评轨迹对比**:无法对比用户多次测评的心理变化情况
|
||||
- ❌ **批量权限设置**:对测评用户权限进行批量设置的功能未实现
|
||||
|
||||
## 项目状态总结
|
||||
|
||||
### 完成度评估
|
||||
- **核心测评流程**:✅ 已完成95%
|
||||
- **扩展功能模块**:⚠️ 部分完成(3/6模块)
|
||||
- **特殊标记功能**:❌ 多个★标记功能未实现
|
||||
|
||||
### 整体状况
|
||||
系统已实现基本的心理测评功能闭环,包括量表管理、题目配置、测评执行和报告生成。然而,多个需求文档中标★的关键功能(如语音读题、危机预警)尚未实现,一些扩展功能(如心理网站前台、完整的二维码功能)仍在开发中。
|
||||
|
||||
### 下一步建议
|
||||
1. 优先实现标★的核心需求功能,特别是语音读题和危机预警
|
||||
2. 完善已部分完成的功能模块
|
||||
3. 开发数据统计和分析功能
|
||||
4. 实现开放式API接口,满足系统集成需求
|
||||
|
||||
10
z_Project change/.obsidian/workspace.json
vendored
10
z_Project change/.obsidian/workspace.json
vendored
|
|
@ -11,14 +11,10 @@
|
|||
"id": "1e7a449906eec160",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "1-开发计划.md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"type": "empty",
|
||||
"state": {},
|
||||
"icon": "lucide-file",
|
||||
"title": "1-开发计划"
|
||||
"title": "新标签页"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,419 +0,0 @@
|
|||
# 心理测评系统 - 功能使用指南
|
||||
|
||||
## 📋 目录
|
||||
1. [新用户使用流程](#新用户使用流程)
|
||||
2. [管理员使用流程](#管理员使用流程)
|
||||
3. [功能测试清单](#功能测试清单)
|
||||
4. [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 👤 新用户使用流程
|
||||
|
||||
### 步骤1:登录系统
|
||||
1. 访问系统首页(通常是 `http://localhost:80` 或部署后的地址)
|
||||
2. 使用账号密码登录(如果没有账号,需要管理员创建)
|
||||
|
||||
### 步骤2:开始心理测评
|
||||
|
||||
#### 2.1 进入测评管理
|
||||
- 登录后,在左侧菜单找到 **"心理测评管理"** → **"测评管理"**
|
||||
- 或直接访问路由:`/psychology/assessment`
|
||||
|
||||
#### 2.2 开始新测评
|
||||
1. 点击页面上的 **"开始测评"** 按钮
|
||||
2. 进入测评选择页面(`/psychology/assessment/start`)
|
||||
|
||||
#### 2.3 填写测评信息
|
||||
在测评选择页面:
|
||||
- **选择量表**:从下拉框中选择想要测试的量表
|
||||
- **填写被测评人信息**:
|
||||
- 姓名(必填)
|
||||
- 性别(男/女)
|
||||
- 年龄(可选)
|
||||
- 手机号(可选)
|
||||
|
||||
#### 2.4 开始答题
|
||||
1. 点击 **"开始测评"** 按钮
|
||||
2. 系统会自动跳转到答题页面(`/psychology/assessment/taking`)
|
||||
|
||||
#### 2.5 答题界面功能
|
||||
- **题目显示**:显示当前题目和选项
|
||||
- **进度条**:显示答题进度
|
||||
- **导航按钮**:
|
||||
- **上一题**:返回上一题
|
||||
- **下一题**:进入下一题
|
||||
- **暂停测评**:保存当前进度,稍后继续
|
||||
- **提交测评**:完成所有题目后提交
|
||||
|
||||
#### 2.6 查看测评结果
|
||||
- 提交测评后,系统会自动生成测评报告
|
||||
- 返回测评列表页面,找到刚才完成的测评
|
||||
- 点击 **"查看报告"** 按钮查看详细结果
|
||||
|
||||
---
|
||||
|
||||
### 步骤3:查看心理网站内容
|
||||
|
||||
#### 3.1 访问心理网站内容(需要管理员配置)
|
||||
**目前状态**:心理网站内容管理功能已在后台实现,但**前台展示页面尚未开发**。
|
||||
|
||||
**临时方案**:
|
||||
1. 可以通过后台管理查看内容:**"心理网站管理"** → **"网站内容管理"**
|
||||
2. 或者等待前台展示页面开发完成
|
||||
|
||||
---
|
||||
|
||||
## 🔧 管理员使用流程
|
||||
|
||||
### 一、量表管理
|
||||
|
||||
#### 1.1 创建新量表
|
||||
**路径**:`心理测评管理` → `量表管理`
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击 **"新增"** 按钮
|
||||
2. 填写量表基本信息:
|
||||
- **量表编码**:唯一标识,如 `SAI_001`
|
||||
- **量表名称**:如 `焦虑自评量表`
|
||||
- **量表类型**:选择类型(情绪、人格、行为等)
|
||||
- **量表简介**:简短介绍
|
||||
- **量表描述**:详细描述
|
||||
- **题目数量**:预计题目数
|
||||
- **预计完成时间**:分钟数
|
||||
- **适用人群**:如 `一般人群`
|
||||
- **作者**:量表作者
|
||||
- **来源**:量表来源
|
||||
- **状态**:选择 `正常` 或 `停用`
|
||||
3. 点击 **"确定"** 保存
|
||||
|
||||
#### 1.2 编辑量表
|
||||
1. 在量表列表中找到要编辑的量表
|
||||
2. 点击 **"修改"** 按钮
|
||||
3. 修改信息后点击 **"确定"** 保存
|
||||
|
||||
#### 1.3 删除量表
|
||||
1. 勾选要删除的量表
|
||||
2. 点击 **"删除"** 按钮
|
||||
3. 确认删除
|
||||
|
||||
---
|
||||
|
||||
### 二、题目管理
|
||||
|
||||
#### 2.1 为量表添加题目
|
||||
**路径**:`心理测评管理` → `量表管理` → 点击某个量表的 **"题目管理"** 按钮
|
||||
|
||||
**操作步骤**:
|
||||
1. 在题目管理页面点击 **"新增"** 按钮
|
||||
2. 填写题目信息:
|
||||
- **题号**:题目序号
|
||||
- **题目内容**:题目文本
|
||||
- **题目类型**:
|
||||
- `single`:单选题
|
||||
- `multiple`:多选题
|
||||
- `matrix`:矩阵题
|
||||
- **是否必答**:是/否
|
||||
- **排序顺序**:数字越小越靠前
|
||||
3. 点击 **"确定"** 保存题目
|
||||
4. 为题目添加选项(见下方)
|
||||
|
||||
#### 2.2 为题目添加选项
|
||||
1. 在题目列表中,找到要添加选项的题目
|
||||
2. 点击该题目行的 **"选项管理"** 或相关按钮
|
||||
3. 在选项管理页面点击 **"新增"** 按钮
|
||||
4. 填写选项信息:
|
||||
- **选项编码**:如 `A`、`B`、`C`、`D`
|
||||
- **选项内容**:选项文本
|
||||
- **选项分值**:选择该选项的得分
|
||||
- **排序顺序**:选项显示顺序
|
||||
5. 点击 **"确定"** 保存
|
||||
|
||||
**选项示例**(5点量表):
|
||||
- A. 完全不符合(1分)
|
||||
- B. 不太符合(2分)
|
||||
- C. 一般(3分)
|
||||
- D. 比较符合(4分)
|
||||
- E. 完全符合(5分)
|
||||
|
||||
---
|
||||
|
||||
### 三、因子与计分规则
|
||||
|
||||
#### 3.1 创建因子
|
||||
**路径**:`心理测评管理` → `量表管理` → 点击某个量表的 **"因子管理"** 按钮
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击 **"新增"** 按钮
|
||||
2. 填写因子信息:
|
||||
- **因子编码**:如 `ANXIETY`(焦虑)、`DEPRESSION`(抑郁)
|
||||
- **因子名称**:如 `焦虑因子`、`抑郁因子`
|
||||
- **因子描述**:因子说明
|
||||
- **计算方式**:
|
||||
- `sum`:求和
|
||||
- `average`:平均
|
||||
- `weighted_sum`:加权求和
|
||||
3. 点击 **"确定"** 保存
|
||||
|
||||
#### 3.2 配置因子计分规则
|
||||
1. 在因子列表中,找到要配置的因子
|
||||
2. 点击 **"计分规则"** 或相关按钮
|
||||
3. 选择该因子包含的题目:
|
||||
- 勾选属于该因子的题目
|
||||
- 设置权重(默认1.0)
|
||||
- 设置计算类型(sum/average)
|
||||
4. 保存规则
|
||||
|
||||
---
|
||||
|
||||
### 四、设定解读方式(结果解释)
|
||||
|
||||
#### 4.1 创建结果解释规则
|
||||
**路径**:`心理测评管理` → `结果解释管理`(如果菜单存在)
|
||||
|
||||
**操作步骤**:
|
||||
1. 选择要配置的量表和因子
|
||||
2. 点击 **"新增"** 按钮
|
||||
3. 填写解释规则:
|
||||
- **分值范围**:
|
||||
- 最低分:如 `0`
|
||||
- 最高分:如 `20`
|
||||
- **等级**:`low`(低)、`medium`(中)、`high`(高)
|
||||
- **等级名称**:如 `轻度焦虑`、`中度焦虑`、`重度焦虑`
|
||||
- **解释标题**:如 `您的焦虑水平为轻度`
|
||||
- **解释内容**:详细解释文本
|
||||
- **建议**:针对性的建议
|
||||
4. 点击 **"确定"** 保存
|
||||
|
||||
**示例配置**:
|
||||
```
|
||||
因子:焦虑因子
|
||||
分值范围:0-20
|
||||
等级:low
|
||||
等级名称:轻度焦虑
|
||||
解释内容:您的焦虑水平处于正常范围,略高于平均水平...
|
||||
建议:保持当前状态,适当放松...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 五、心理网站内容管理
|
||||
|
||||
#### 5.1 创建栏目
|
||||
**路径**:`心理网站管理` → `网站栏目管理`
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击 **"新增"** 按钮
|
||||
2. 填写栏目信息:
|
||||
- **栏目名称**:如 `心理健康知识`、`心理测试`、`专家问答`
|
||||
- **栏目编码**:如 `health_knowledge`
|
||||
- **上级栏目**:选择父栏目(可为空,表示一级栏目)
|
||||
- **栏目类型**:选择类型
|
||||
- **图标**:栏目图标
|
||||
- **描述**:栏目描述
|
||||
- **排序顺序**:数字越小越靠前
|
||||
- **状态**:正常/停用
|
||||
3. 点击 **"确定"** 保存
|
||||
|
||||
#### 5.2 添加文章内容
|
||||
**路径**:`心理网站管理` → `网站内容管理`
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击 **"新增"** 按钮
|
||||
2. 填写内容信息:
|
||||
- **内容类型**:文章/公告/轮播/链接
|
||||
- **归属栏目**:选择栏目(必填)
|
||||
- **标题**:文章标题(必填)
|
||||
- **副标题**:可选
|
||||
- **摘要**:文章摘要
|
||||
- **内容**:正文内容(富文本编辑器)
|
||||
- **封面图片**:上传封面
|
||||
- **作者**:文章作者
|
||||
- **来源**:文章来源
|
||||
- **状态**:正常/停用
|
||||
- **是否置顶**:是/否
|
||||
- **是否推荐**:是/否
|
||||
3. 点击 **"确定"** 保存
|
||||
|
||||
#### 5.3 管理评论
|
||||
**路径**:`心理网站管理` → `网站评论管理`
|
||||
|
||||
**功能**:
|
||||
- 查看所有评论
|
||||
- 审核评论(正常/停用)
|
||||
- 删除不当评论
|
||||
|
||||
---
|
||||
|
||||
## ✅ 功能测试清单
|
||||
|
||||
### 新用户功能测试
|
||||
|
||||
- [ ] **登录系统**
|
||||
- [ ] 使用有效账号登录
|
||||
- [ ] 验证登录成功
|
||||
|
||||
- [ ] **开始测评**
|
||||
- [ ] 进入测评管理页面
|
||||
- [ ] 点击"开始测评"按钮
|
||||
- [ ] 选择量表
|
||||
- [ ] 填写被测评人信息
|
||||
- [ ] 成功开始测评
|
||||
|
||||
- [ ] **答题过程**
|
||||
- [ ] 查看题目和选项
|
||||
- [ ] 选择答案
|
||||
- [ ] 使用"上一题"按钮
|
||||
- [ ] 使用"下一题"按钮
|
||||
- [ ] 查看进度条
|
||||
- [ ] 暂停测评
|
||||
- [ ] 恢复暂停的测评
|
||||
- [ ] 提交测评
|
||||
|
||||
- [ ] **查看结果**
|
||||
- [ ] 查看测评报告
|
||||
- [ ] 验证报告内容正确性
|
||||
|
||||
---
|
||||
|
||||
### 管理员功能测试
|
||||
|
||||
#### 量表管理
|
||||
- [ ] **创建量表**
|
||||
- [ ] 新增量表成功
|
||||
- [ ] 验证量表编码唯一性
|
||||
- [ ] 查看量表列表
|
||||
|
||||
- [ ] **编辑量表**
|
||||
- [ ] 修改量表信息
|
||||
- [ ] 保存成功
|
||||
|
||||
- [ ] **删除量表**
|
||||
- [ ] 删除单个量表
|
||||
- [ ] 批量删除量表
|
||||
|
||||
#### 题目管理
|
||||
- [ ] **添加题目**
|
||||
- [ ] 新增单选题
|
||||
- [ ] 新增多选题
|
||||
- [ ] 新增矩阵题
|
||||
|
||||
- [ ] **管理选项**
|
||||
- [ ] 为题目添加选项
|
||||
- [ ] 设置选项分值
|
||||
- [ ] 编辑选项
|
||||
- [ ] 删除选项
|
||||
|
||||
#### 因子管理
|
||||
- [ ] **创建因子**
|
||||
- [ ] 新增因子
|
||||
- [ ] 配置因子计分规则
|
||||
- [ ] 关联题目到因子
|
||||
|
||||
#### 结果解释
|
||||
- [ ] **配置解释规则**
|
||||
- [ ] 创建解释规则
|
||||
- [ ] 设置分值范围
|
||||
- [ ] 填写解释内容
|
||||
- [ ] 验证报告生成时使用解释规则
|
||||
|
||||
#### 心理网站管理
|
||||
- [ ] **栏目管理**
|
||||
- [ ] 创建一级栏目
|
||||
- [ ] 创建二级栏目
|
||||
- [ ] 编辑栏目
|
||||
- [ ] 删除栏目
|
||||
|
||||
- [ ] **内容管理**
|
||||
- [ ] 创建文章
|
||||
- [ ] 选择栏目
|
||||
- [ ] 编辑内容
|
||||
- [ ] 设置封面图片
|
||||
- [ ] 发布文章
|
||||
|
||||
- [ ] **评论管理**
|
||||
- [ ] 查看评论
|
||||
- [ ] 审核评论
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q1: 如何测试测评功能?
|
||||
**A**:
|
||||
1. 先以管理员身份登录
|
||||
2. 创建一个测试量表
|
||||
3. 为量表添加至少3-5道题目
|
||||
4. 为每道题目添加选项
|
||||
5. 创建因子并配置计分规则
|
||||
6. 配置结果解释规则
|
||||
7. 退出,用普通用户账号登录
|
||||
8. 开始测评并答题
|
||||
|
||||
### Q2: 测评报告如何生成?
|
||||
**A**:
|
||||
1. 提交测评后,系统会自动计算得分
|
||||
2. 根据因子计分规则计算各因子分
|
||||
3. 匹配结果解释规则
|
||||
4. 生成HTML格式的报告
|
||||
5. 可以在"测评报告"页面查看
|
||||
|
||||
### Q3: 如何查看心理网站内容?
|
||||
**A**:
|
||||
**目前状态**:前台展示页面尚未开发,只能通过后台管理查看。
|
||||
|
||||
**解决方案**:
|
||||
1. 临时方案:在后台"网站内容管理"中查看
|
||||
2. 完整方案:需要开发前台展示页面(预计1-2天开发时间)
|
||||
|
||||
### Q4: 量表状态"停用"是什么意思?
|
||||
**A**:
|
||||
- 停用的量表不会在"开始测评"页面的量表列表中显示
|
||||
- 但已有的测评记录不受影响
|
||||
- 管理员可以在量表管理中修改状态
|
||||
|
||||
### Q5: 如何批量导入题目?
|
||||
**A**:
|
||||
**目前状态**:批量导入功能尚未实现,需要手动添加。
|
||||
|
||||
**建议**:
|
||||
- 先创建少量题目测试功能
|
||||
- 后续可以开发Excel导入功能
|
||||
|
||||
---
|
||||
|
||||
## 📝 测试建议
|
||||
|
||||
### 第一步:准备测试数据
|
||||
1. 创建1-2个测试量表
|
||||
2. 每个量表添加5-10道题目
|
||||
3. 每道题目添加4-5个选项
|
||||
4. 创建1-2个因子
|
||||
5. 配置结果解释规则
|
||||
|
||||
### 第二步:测试完整流程
|
||||
1. 以新用户身份登录
|
||||
2. 开始测评
|
||||
3. 完成答题
|
||||
4. 查看报告
|
||||
5. 验证报告内容
|
||||
|
||||
### 第三步:测试管理功能
|
||||
1. 修改量表信息
|
||||
2. 添加新题目
|
||||
3. 修改因子配置
|
||||
4. 添加网站内容
|
||||
|
||||
---
|
||||
|
||||
## 🚧 待开发功能
|
||||
|
||||
1. **前台展示页面**(新用户查看心理网站内容)
|
||||
2. **批量导入题目**(Excel导入)
|
||||
3. **报告PDF导出**
|
||||
4. **测评数据统计图表**
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v1.0
|
||||
**最后更新**:2025-11-04
|
||||
BIN
z_Project change/学习项目.pdf
Normal file
BIN
z_Project change/学习项目.pdf
Normal file
Binary file not shown.
182
z_Project change/进度汇总/18-导出功能说明.md
Normal file
182
z_Project change/进度汇总/18-导出功能说明.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# 导出功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
系统提供了量表导出和报告导出功能,支持单个或批量导出数据。
|
||||
|
||||
## 1. 量表导出功能
|
||||
|
||||
### 功能位置
|
||||
- **页面路径**:心理测评管理 > 量表管理
|
||||
- **按钮位置**:量表列表页面顶部工具栏
|
||||
|
||||
### 功能说明
|
||||
|
||||
#### 1.1 导出格式
|
||||
- **格式**:JSON格式
|
||||
- **文件扩展名**:`.json`
|
||||
- **编码**:UTF-8
|
||||
|
||||
#### 1.2 导出内容
|
||||
导出的JSON文件包含量表的完整数据,包括:
|
||||
- **量表基本信息**:量表编码、名称、类型、版本、描述等
|
||||
- **因子列表**:所有因子及其配置信息
|
||||
- **因子计分规则**:每个因子的计分规则,包含题目序号映射
|
||||
- **题目列表**:所有题目及其配置信息
|
||||
- **选项列表**:每个题目的所有选项
|
||||
- **解释配置**:因子解释和总体解释配置(包含factorCode用于导入时映射)
|
||||
- **预警规则**:预警规则配置(包含factorCode用于导入时映射)
|
||||
|
||||
#### 1.3 使用方法
|
||||
|
||||
**方式一:批量导出(推荐)**
|
||||
1. 在量表列表页面,勾选需要导出的量表(可多选)
|
||||
2. 点击"导出"按钮
|
||||
3. 系统会导出所有选中的量表,生成一个JSON文件
|
||||
|
||||
**方式二:导出所有量表**
|
||||
1. 不勾选任何量表
|
||||
2. 点击"导出"按钮
|
||||
3. 系统会导出当前查询条件下的所有量表
|
||||
|
||||
#### 1.4 导出文件命名规则
|
||||
- **单个量表导出**:`{量表名称}_{时间戳}.json`
|
||||
- **批量导出**:`量表批量导出_{时间戳}.json`
|
||||
|
||||
#### 1.5 注意事项
|
||||
- 导出的JSON文件可以直接用于导入功能
|
||||
- 导出的数据包含完整的量表配置,可用于备份和迁移
|
||||
- 导出的factorCode信息可以确保导入时正确映射因子
|
||||
|
||||
## 2. 报告导出功能
|
||||
|
||||
### 功能位置
|
||||
- **页面路径**:心理测评管理 > 测评报告
|
||||
- **按钮位置**:报告列表页面顶部工具栏
|
||||
|
||||
### 功能说明
|
||||
|
||||
#### 2.1 导出格式
|
||||
- **格式**:Excel格式(.xlsx)
|
||||
- **文件扩展名**:`.xlsx`
|
||||
- **编码**:UTF-8
|
||||
|
||||
#### 2.2 导出内容
|
||||
导出的Excel文件包含报告的以下信息:
|
||||
- **报告ID**:报告的唯一标识
|
||||
- **测评ID**:关联的测评记录ID
|
||||
- **报告标题**:报告的标题
|
||||
- **报告类型**:标准报告/详细报告/简要报告
|
||||
- **报告摘要**:报告的摘要信息
|
||||
- **报告内容**:报告正文内容(HTML标签已转换为纯文本)
|
||||
- **生成状态**:已生成/未生成
|
||||
- **生成时间**:报告生成的时间
|
||||
- **创建时间**:报告创建的时间
|
||||
|
||||
#### 2.3 使用方法
|
||||
|
||||
**方式一:批量导出(推荐)**
|
||||
1. 在报告列表页面,勾选需要导出的报告(可多选)
|
||||
2. 点击"导出"按钮
|
||||
3. 系统会导出所有选中的报告,生成一个Excel文件
|
||||
|
||||
**方式二:按条件导出**
|
||||
1. 使用搜索条件筛选报告
|
||||
2. 不勾选任何报告
|
||||
3. 点击"导出"按钮
|
||||
4. 系统会导出所有符合查询条件的报告
|
||||
|
||||
#### 2.4 导出文件命名规则
|
||||
- 默认文件名:`报告导出_{时间戳}.xlsx`
|
||||
|
||||
#### 2.5 注意事项
|
||||
- Excel文件中的报告内容已去除HTML标签,转换为纯文本
|
||||
- 报告内容较长时,Excel单元格会自动换行显示
|
||||
- 可以方便地进行数据分析和统计
|
||||
|
||||
## 3. 权限要求
|
||||
|
||||
### 量表导出权限
|
||||
- **权限代码**:`psychology:scale:export`
|
||||
- **权限名称**:量表导出
|
||||
- **默认角色**:管理员
|
||||
|
||||
### 报告导出权限
|
||||
- **权限代码**:`psychology:report:export`
|
||||
- **权限名称**:报告导出
|
||||
- **默认角色**:管理员
|
||||
|
||||
## 4. 技术实现
|
||||
|
||||
### 4.1 后端实现
|
||||
|
||||
#### 量表导出
|
||||
- **Controller**:`PsyScaleController.exportScales()`
|
||||
- **Service**:`PsyScaleService.exportScales()`
|
||||
- **返回格式**:JSON文件下载
|
||||
|
||||
#### 报告导出
|
||||
- **Controller**:`PsyAssessmentReportController.exportReports()`
|
||||
- **Service**:使用`ExcelUtil`工具类
|
||||
- **返回格式**:Excel文件下载
|
||||
|
||||
### 4.2 前端实现
|
||||
|
||||
#### 量表导出
|
||||
- **API方法**:`exportScale(scaleIds)`
|
||||
- **文件类型**:`application/json`
|
||||
- **下载方式**:Blob对象下载
|
||||
|
||||
#### 报告导出
|
||||
- **API方法**:`exportReport(reportIds, queryParams)`
|
||||
- **文件类型**:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
||||
- **下载方式**:Blob对象下载
|
||||
|
||||
## 5. 使用示例
|
||||
|
||||
### 示例1:导出SCL-90量表
|
||||
|
||||
1. 进入"量表管理"页面
|
||||
2. 找到"症状自评量表SCL-90"
|
||||
3. 勾选该量表
|
||||
4. 点击"导出"按钮
|
||||
5. 下载生成的JSON文件
|
||||
|
||||
### 示例2:导出所有已生成的报告
|
||||
|
||||
1. 进入"测评报告"页面
|
||||
2. 在"生成状态"筛选中选择"已生成"
|
||||
3. 点击"搜索"按钮
|
||||
4. 不勾选任何报告(或全选)
|
||||
5. 点击"导出"按钮
|
||||
6. 下载生成的Excel文件
|
||||
|
||||
## 6. 常见问题
|
||||
|
||||
### Q1: 导出失败怎么办?
|
||||
**A**: 请检查:
|
||||
1. 是否有导出权限
|
||||
2. 网络连接是否正常
|
||||
3. 浏览器是否支持文件下载
|
||||
4. 查看浏览器控制台错误信息
|
||||
|
||||
### Q2: 导出的JSON文件可以导入吗?
|
||||
**A**: 可以。导出的JSON文件完全符合导入格式要求,可以直接用于导入功能。
|
||||
|
||||
### Q3: 导出的Excel文件如何打开?
|
||||
**A**: 可以使用Microsoft Excel、WPS Office、Google Sheets等软件打开。
|
||||
|
||||
### Q4: 可以导出其他格式吗?
|
||||
**A**: 目前支持:
|
||||
- 量表导出:JSON格式
|
||||
- 报告导出:Excel格式
|
||||
- 未来可能会支持更多格式(如PDF、CSV等)
|
||||
|
||||
## 7. 更新日志
|
||||
|
||||
### 2024-01-XX
|
||||
- ✅ 新增量表导出功能(JSON格式)
|
||||
- ✅ 新增报告导出功能(Excel格式)
|
||||
- ✅ 支持单个和批量导出
|
||||
- ✅ 支持按查询条件导出
|
||||
|
||||
203
z_Project change/进度汇总/19-二维码功能使用说明.md
Normal file
203
z_Project change/进度汇总/19-二维码功能使用说明.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# 二维码功能使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
系统提供了二维码功能,方便用户通过扫码快速访问测评和查看报告。支持以下功能:
|
||||
- **量表测评二维码**:扫码后直接开始指定量表的测评
|
||||
- **报告查看二维码**:扫码后直接查看指定报告
|
||||
- **测评报告二维码**:通过测评ID生成二维码,扫码后查看对应的报告
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **自动跳转**:扫码后自动识别二维码类型,跳转到对应页面
|
||||
2. **扫码统计**:记录扫码次数,方便统计使用情况
|
||||
3. **过期管理**:支持设置二维码过期时间
|
||||
4. **状态管理**:支持启用/禁用二维码
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 生成量表测评二维码
|
||||
|
||||
#### 在量表管理页面生成
|
||||
|
||||
1. 进入 **心理测评 -> 量表管理**
|
||||
2. 在量表列表中找到目标量表
|
||||
3. 点击操作列的 **二维码** 按钮
|
||||
4. 系统自动生成二维码并显示在对话框中
|
||||
5. 可以下载二维码图片或直接打印
|
||||
|
||||
**二维码内容**:
|
||||
- 扫码后会跳转到该量表的测评开始页面
|
||||
- 如果用户未登录,会提示先登录
|
||||
|
||||
### 2. 生成报告查看二维码
|
||||
|
||||
#### 方法一:在报告管理页面生成(待实现)
|
||||
|
||||
1. 进入 **心理测评 -> 报告管理**
|
||||
2. 在报告列表中找到目标报告
|
||||
3. 点击操作列的 **二维码** 按钮
|
||||
4. 系统自动生成二维码
|
||||
|
||||
**二维码内容**:
|
||||
- 扫码后会跳转到该报告的详情页面
|
||||
|
||||
#### 方法二:在测评管理页面生成(待实现)
|
||||
|
||||
1. 进入 **心理测评 -> 测评管理**
|
||||
2. 在测评列表中找到已完成测评的记录
|
||||
3. 点击操作列的 **二维码** 按钮
|
||||
4. 系统自动生成二维码
|
||||
|
||||
**二维码内容**:
|
||||
- 扫码后会跳转到该测评对应的报告详情页面
|
||||
|
||||
### 3. 扫描二维码
|
||||
|
||||
#### 扫描方式
|
||||
|
||||
1. **手机扫码**:
|
||||
- 使用手机微信、支付宝等应用扫描二维码
|
||||
- 或使用专门的二维码扫描应用
|
||||
|
||||
2. **电脑扫码**:
|
||||
- 在浏览器中访问二维码URL
|
||||
- 格式:`http://域名/psychology/qrcode/scan/{二维码编码}`
|
||||
|
||||
#### 扫描流程
|
||||
|
||||
1. 用户扫描二维码
|
||||
2. 系统自动识别二维码类型和目标
|
||||
3. 记录扫码次数
|
||||
4. 检查二维码状态和过期时间
|
||||
5. 自动跳转到对应页面:
|
||||
- **测评类型**:跳转到测评开始页面(如果指定了量表,直接进入该量表测评)
|
||||
- **报告类型**:跳转到报告详情页面
|
||||
|
||||
## 二维码管理
|
||||
|
||||
### 查看所有二维码
|
||||
|
||||
1. 进入 **心理测评 -> 二维码管理**
|
||||
2. 可以查看所有已生成的二维码
|
||||
3. 查看二维码类型、目标、扫码次数等信息
|
||||
|
||||
### 重新生成二维码
|
||||
|
||||
1. 在二维码管理页面
|
||||
2. 点击 **重新生成** 按钮
|
||||
3. 系统会重新生成二维码图片
|
||||
|
||||
### 删除二维码
|
||||
|
||||
1. 在二维码管理页面
|
||||
2. 选择要删除的二维码
|
||||
3. 点击 **删除** 按钮
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 二维码类型
|
||||
|
||||
- `test`:测评类型,用于开始测评
|
||||
- `view_report`:查看报告类型,用于查看报告
|
||||
- `register`:注册类型,用于用户注册
|
||||
- `login`:登录类型,用于用户登录
|
||||
|
||||
### 目标类型
|
||||
|
||||
- `scale`:量表,关联量表ID
|
||||
- `assessment`:测评,关联测评ID
|
||||
- `report`:报告,关联报告ID
|
||||
|
||||
### 二维码状态
|
||||
|
||||
- `0`:有效
|
||||
- `1`:无效
|
||||
- `2`:已过期
|
||||
|
||||
### API接口
|
||||
|
||||
#### 扫描二维码(公开接口)
|
||||
```
|
||||
GET /psychology/qrcode/scan/{qrcodeCode}
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"qrcode": {
|
||||
"qrcodeId": 1,
|
||||
"qrcodeCode": "abc123",
|
||||
"qrcodeType": "test",
|
||||
"targetType": "scale",
|
||||
"targetId": 1,
|
||||
"scanCount": 5
|
||||
},
|
||||
"redirectUrl": "/psychology/assessment/start?scaleId=1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 生成量表测评二维码
|
||||
```
|
||||
POST /psychology/qrcode/generate/scale?scaleId={scaleId}
|
||||
```
|
||||
|
||||
#### 生成报告查看二维码
|
||||
```
|
||||
POST /psychology/qrcode/generate/report?reportId={reportId}
|
||||
```
|
||||
|
||||
#### 生成测评报告二维码
|
||||
```
|
||||
POST /psychology/qrcode/generate/assessment?assessmentId={assessmentId}
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1:线下测评
|
||||
|
||||
1. 管理员生成量表测评二维码
|
||||
2. 打印二维码并张贴在测评现场
|
||||
3. 用户扫码后直接开始测评
|
||||
4. 无需输入量表名称或选择量表
|
||||
|
||||
### 场景2:报告分享
|
||||
|
||||
1. 管理员生成报告查看二维码
|
||||
2. 通过微信、邮件等方式分享给用户
|
||||
3. 用户扫码后直接查看报告
|
||||
4. 方便快捷,无需登录系统查找
|
||||
|
||||
### 场景3:批量测评
|
||||
|
||||
1. 为每个量表生成独立二维码
|
||||
2. 将二维码打印在不同位置
|
||||
3. 用户根据需求扫码选择对应的量表
|
||||
4. 提高测评效率
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **二维码有效期**:建议设置二维码过期时间,避免长期有效造成安全隐患
|
||||
2. **权限控制**:生成二维码需要相应权限,扫码查看报告可能需要登录
|
||||
3. **网络环境**:确保二维码URL可以正常访问
|
||||
4. **扫码统计**:系统会记录扫码次数,可用于统计分析
|
||||
|
||||
## 后续优化
|
||||
|
||||
1. 在报告管理页面添加生成二维码功能
|
||||
2. 在测评管理页面添加生成二维码功能
|
||||
3. 支持自定义二维码样式(Logo、颜色等)
|
||||
4. 支持批量生成二维码
|
||||
5. 支持二维码短链接功能
|
||||
6. 支持二维码访问权限控制
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 后端接口:`ry-news-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQrcodeController.java`
|
||||
- 前端页面:`ruoyi-ui/src/views/psychology/qrcode/scan.vue`
|
||||
- API接口:`ruoyi-ui/src/api/psychology/qrcode.js`
|
||||
- 服务实现:`ry-news-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyQrcodeServiceImpl.java`
|
||||
209
z_Project change/进度汇总/20-自定义问卷功能增强开发记录.md
Normal file
209
z_Project change/进度汇总/20-自定义问卷功能增强开发记录.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# 自定义问卷功能增强开发记录
|
||||
|
||||
## 📋 任务概述
|
||||
|
||||
**开发日期**: 2025-01-XX
|
||||
**模块名称**: 自定义问卷功能增强
|
||||
**任务类型**: 功能增强与完善
|
||||
|
||||
---
|
||||
|
||||
## 🎯 需求说明
|
||||
|
||||
### 1. 菜单统一
|
||||
- ✅ 删除"问卷管理"菜单,统一使用"自定义问卷"
|
||||
|
||||
### 2. 问卷测评功能
|
||||
- 自定义问卷要像量表一样可以分配给用户进行测量
|
||||
- 管理员可以设置对应的得分
|
||||
- 在解释配置中可以设定对应的分数进行说明测量之后的结果
|
||||
|
||||
### 3. 题目类型支持
|
||||
- 支持多种题目类型:单选、多选、判断、填空、排序、计算、简答、问答、作文
|
||||
|
||||
### 4. 主观题评分
|
||||
- 管理员能汇总主观题进行评分
|
||||
|
||||
### 5. 成绩排名
|
||||
- 管理员能查看所有问卷人员以及对应的分数和排名
|
||||
- 排名从上往下是第一名往下排队
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成工作
|
||||
|
||||
### 1. 菜单统一 ✅
|
||||
- ✅ 修改SQL脚本,将"问卷管理"菜单改为"自定义问卷"
|
||||
- ✅ 修改前端路由,将"问卷管理"改为"自定义问卷"
|
||||
- ✅ 更新菜单图标为`edit`
|
||||
|
||||
**修改文件**:
|
||||
- `sql/psychological_system_complete.sql` - 菜单配置SQL
|
||||
- `ruoyi-ui/src/router/index.js` - 前端路由配置
|
||||
|
||||
### 2. 数据库表结构增强 ✅
|
||||
- ✅ 创建问卷答案详情表 `psy_questionnaire_answer_detail`
|
||||
- 存储每道题的答案
|
||||
- 支持客观题和主观题
|
||||
- 支持管理员评分功能
|
||||
- ✅ 修改解释配置表 `psy_result_interpretation`
|
||||
- 添加 `questionnaire_id` 字段,支持问卷解释配置
|
||||
- 添加索引和外键约束
|
||||
|
||||
**新增表结构**:
|
||||
```sql
|
||||
-- 问卷答案详情表
|
||||
psy_questionnaire_answer_detail
|
||||
- detail_id: 详情ID
|
||||
- answer_id: 答案ID(关联问卷答题记录)
|
||||
- item_id: 题目ID
|
||||
- option_id: 选项ID(单选/判断)
|
||||
- option_ids: 选项ID列表(多选)
|
||||
- answer_text: 文本答案(填空、简答、问答、作文)
|
||||
- answer_score: 答案得分
|
||||
- is_subjective: 是否主观题
|
||||
- is_scored: 是否已评分
|
||||
- scored_by: 评分人
|
||||
- scored_time: 评分时间
|
||||
```
|
||||
|
||||
**修改表结构**:
|
||||
```sql
|
||||
-- 解释配置表
|
||||
psy_result_interpretation
|
||||
- questionnaire_id: 问卷ID(新增)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚧 待完成工作
|
||||
|
||||
### 1. 问卷测评功能(类似量表测评)
|
||||
- [ ] 创建问卷测评实体类 `PsyQuestionnaireAnswerDetail`
|
||||
- [ ] 创建问卷测评Mapper和Service
|
||||
- [ ] 创建问卷测评Controller
|
||||
- [ ] 开始问卷测评接口
|
||||
- [ ] 获取问卷题目接口
|
||||
- [ ] 保存答案接口
|
||||
- [ ] 提交问卷接口
|
||||
- [ ] 自动计分服务(客观题)
|
||||
- [ ] 创建问卷测评前端页面
|
||||
- [ ] 问卷列表页面
|
||||
- [ ] 开始问卷页面
|
||||
- [ ] 答题页面(支持多种题型)
|
||||
- [ ] 提交问卷页面
|
||||
|
||||
### 2. 多种题目类型支持
|
||||
- [ ] 题目类型枚举定义
|
||||
- [ ] radio: 单选
|
||||
- [ ] checkbox: 多选
|
||||
- [ ] boolean: 判断
|
||||
- [ ] input: 填空
|
||||
- [ ] sort: 排序
|
||||
- [ ] calculate: 计算
|
||||
- [ ] text: 简答
|
||||
- [ ] textarea: 问答
|
||||
- [ ] essay: 作文
|
||||
- [ ] 题目管理页面增强
|
||||
- [ ] 题目类型选择器
|
||||
- [ ] 不同题型的编辑界面
|
||||
- [ ] 选项配置(单选、多选、判断)
|
||||
- [ ] 文本答案配置(填空、简答、问答、作文)
|
||||
|
||||
### 3. 主观题评分功能
|
||||
- [ ] 主观题评分管理页面
|
||||
- [ ] 待评分题目列表
|
||||
- [ ] 评分界面
|
||||
- [ ] 批量评分功能
|
||||
- [ ] 主观题评分接口
|
||||
- [ ] 获取待评分题目接口
|
||||
- [ ] 提交评分接口
|
||||
- [ ] 批量评分接口
|
||||
|
||||
### 4. 成绩排名功能
|
||||
- [ ] 成绩排名查询接口
|
||||
- [ ] 按问卷ID查询排名
|
||||
- [ ] 按用户ID查询排名
|
||||
- [ ] 排名计算逻辑(从高到低)
|
||||
- [ ] 成绩排名展示页面
|
||||
- [ ] 排名列表(表格)
|
||||
- [ ] 排名统计图表
|
||||
- [ ] 导出排名数据
|
||||
|
||||
### 5. 解释配置增强
|
||||
- [ ] 解释配置页面增强
|
||||
- [ ] 支持选择问卷
|
||||
- [ ] 问卷解释配置界面
|
||||
- [ ] 解释配置查询接口
|
||||
- [ ] 按问卷ID查询解释
|
||||
- [ ] 按分数范围匹配解释
|
||||
|
||||
---
|
||||
|
||||
## 📁 涉及文件
|
||||
|
||||
### 后端文件
|
||||
- `ry-news-system/src/main/java/com/ddnai/system/domain/psychology/PsyQuestionnaireAnswerDetail.java` (待创建)
|
||||
- `ry-news-system/src/main/java/com/ddnai/system/mapper/psychology/PsyQuestionnaireAnswerDetailMapper.java` (待创建)
|
||||
- `ry-news-system/src/main/java/com/ddnai/system/service/psychology/IPsyQuestionnaireAnswerDetailService.java` (待创建)
|
||||
- `ry-news-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyQuestionnaireAnswerDetailServiceImpl.java` (待创建)
|
||||
- `ry-news-admin/src/main/java/com/ddnai/web/controller/psychology/PsyQuestionnaireAnswerController.java` (待创建)
|
||||
- `ry-news-system/src/main/java/com/ddnai/system/domain/psychology/PsyResultInterpretation.java` (待修改)
|
||||
- `ry-news-system/src/main/resources/mapper/system/psychology/PsyQuestionnaireAnswerDetailMapper.xml` (待创建)
|
||||
|
||||
### 前端文件
|
||||
- `ruoyi-ui/src/views/psychology/questionnaire/answer/index.vue` (待创建)
|
||||
- `ruoyi-ui/src/views/psychology/questionnaire/answer/taking.vue` (待创建)
|
||||
- `ruoyi-ui/src/views/psychology/questionnaire/score/index.vue` (待创建)
|
||||
- `ruoyi-ui/src/views/psychology/questionnaire/rank/index.vue` (待创建)
|
||||
- `ruoyi-ui/src/api/psychology/questionnaire.js` (待修改)
|
||||
|
||||
### 数据库文件
|
||||
- `sql/psychological_system_complete.sql` (已修改)
|
||||
|
||||
---
|
||||
|
||||
## 📝 技术要点
|
||||
|
||||
### 1. 题目类型判断
|
||||
- 客观题:radio、checkbox、boolean、input(有标准答案)、sort、calculate
|
||||
- 主观题:text、textarea、essay、input(无标准答案)
|
||||
|
||||
### 2. 自动计分逻辑
|
||||
- 客观题:提交时自动计算得分
|
||||
- 主观题:需要管理员手动评分
|
||||
|
||||
### 3. 排名计算
|
||||
- 按总分从高到低排序
|
||||
- 相同分数按提交时间排序(先提交的排名靠前)
|
||||
|
||||
### 4. 解释配置匹配
|
||||
- 根据问卷ID和分数范围匹配解释
|
||||
- 支持多个分数区间配置
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. 问卷测评功能要参考量表测评的实现方式
|
||||
2. 题目类型要支持所有9种类型
|
||||
3. 主观题评分要有明确的权限控制
|
||||
4. 排名计算要考虑相同分数的情况
|
||||
5. 解释配置要同时支持量表和问卷
|
||||
|
||||
---
|
||||
|
||||
## 📌 下一步计划
|
||||
|
||||
1. 创建问卷答案详情实体类和Mapper
|
||||
2. 实现问卷测评基础功能(开始、答题、提交)
|
||||
3. 实现多种题目类型的答题界面
|
||||
4. 实现主观题评分功能
|
||||
5. 实现成绩排名功能
|
||||
6. 完善解释配置功能
|
||||
|
||||
---
|
||||
|
||||
**创建时间**: 2025-01-XX
|
||||
**最后更新**: 2025-01-XX
|
||||
|
||||
221
z_Project change/进度汇总/21-问卷功能完整实现方案.md
Normal file
221
z_Project change/进度汇总/21-问卷功能完整实现方案.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# 问卷功能完整实现方案
|
||||
|
||||
## 📋 需求概述
|
||||
|
||||
系统需要支持完整的问卷功能,包括:
|
||||
1. **问卷在量表管理中显示**:创建的问卷能够出现在量表管理中
|
||||
2. **问卷答题功能**:用户填写完问卷后,客观题自动打分,主观题传给管理员进行打分
|
||||
3. **多种题目类型支持**:单选、多选、判断、填空、排序、计算、简答、问答、作文等
|
||||
4. **多种组卷形式**:自助组卷、随机组卷、手动随机相结合
|
||||
5. **成绩排名统计**:客观题系统自动实现打分,成绩自动排名统计
|
||||
|
||||
---
|
||||
|
||||
## 🎯 实现方案
|
||||
|
||||
### 阶段一:让问卷显示在量表管理中
|
||||
|
||||
#### 方案1:统一查询接口(推荐)
|
||||
- 修改量表列表查询,同时返回问卷数据
|
||||
- 添加类型标识字段(`sourceType`: 'scale' 或 'questionnaire')
|
||||
- 前端统一显示,根据类型标识区分操作
|
||||
|
||||
#### 方案2:创建统一视图
|
||||
- 在数据库创建视图,统一量表和问卷
|
||||
- 查询时使用视图
|
||||
|
||||
**选择方案1**,因为更灵活,不需要修改数据库结构。
|
||||
|
||||
---
|
||||
|
||||
### 阶段二:问卷答题功能
|
||||
|
||||
参考量表测评的实现方式:
|
||||
|
||||
1. **开始问卷**:创建问卷答题记录
|
||||
2. **获取题目**:根据组卷方式获取题目列表
|
||||
3. **保存答案**:实时保存用户答案
|
||||
4. **提交问卷**:
|
||||
- 客观题自动计分
|
||||
- 主观题标记为待评分
|
||||
- 计算客观题总分
|
||||
- 更新排名
|
||||
|
||||
---
|
||||
|
||||
### 阶段三:自动打分功能
|
||||
|
||||
#### 客观题类型
|
||||
- **radio(单选)**:根据选项的`is_correct`和`option_score`计分
|
||||
- **checkbox(多选)**:全对得满分,部分对按比例得分
|
||||
- **boolean(判断)**:根据选项的`is_correct`计分
|
||||
- **input(填空)**:如果有标准答案,进行文本匹配(支持模糊匹配)
|
||||
- **sort(排序)**:顺序完全正确得满分,部分正确按比例得分
|
||||
- **calculate(计算)**:数值匹配,允许误差范围
|
||||
|
||||
#### 主观题类型
|
||||
- **text(简答)**:需要管理员评分
|
||||
- **textarea(问答)**:需要管理员评分
|
||||
- **essay(作文)**:需要管理员评分
|
||||
- **input(无标准答案)**:需要管理员评分
|
||||
|
||||
---
|
||||
|
||||
### 阶段四:主观题评分管理
|
||||
|
||||
1. **待评分列表**:显示所有待评分的主观题
|
||||
2. **评分界面**:管理员可以查看题目、答案,进行评分
|
||||
3. **批量评分**:支持批量评分功能
|
||||
4. **评分后更新**:评分后更新总分和排名
|
||||
|
||||
---
|
||||
|
||||
### 阶段五:成绩排名统计
|
||||
|
||||
1. **排名计算**:
|
||||
- 按总分从高到低排序
|
||||
- 相同分数按提交时间排序(先提交的排名靠前)
|
||||
2. **排名更新**:提交问卷或评分后自动更新排名
|
||||
3. **排名查询**:支持按问卷查询排名列表
|
||||
|
||||
---
|
||||
|
||||
## 📁 需要创建/修改的文件
|
||||
|
||||
### 数据库
|
||||
- [x] `psy_questionnaire` - 问卷表(已存在)
|
||||
- [x] `psy_questionnaire_item` - 问卷题目表(已存在)
|
||||
- [x] `psy_questionnaire_option` - 问卷选项表(已存在)
|
||||
- [x] `psy_questionnaire_answer` - 问卷答题记录表(已存在)
|
||||
- [ ] `psy_questionnaire_answer_detail` - 问卷答案详情表(需要创建)
|
||||
|
||||
### 后端Java文件
|
||||
- [ ] `PsyQuestionnaireAnswerDetail.java` - 问卷答案详情实体类
|
||||
- [ ] `PsyQuestionnaireAnswerDetailMapper.java` - Mapper接口
|
||||
- [ ] `PsyQuestionnaireAnswerDetailMapper.xml` - MyBatis映射
|
||||
- [ ] `IPsyQuestionnaireAnswerService.java` - 问卷答题服务接口
|
||||
- [ ] `PsyQuestionnaireAnswerServiceImpl.java` - 问卷答题服务实现
|
||||
- [ ] `PsyQuestionnaireController.java` - 问卷控制器(需要增强)
|
||||
- [ ] `PsyScaleController.java` - 量表控制器(需要修改列表查询)
|
||||
|
||||
### 前端Vue文件
|
||||
- [ ] `questionnaire/taking.vue` - 问卷答题页面
|
||||
- [ ] `questionnaire/start.vue` - 开始问卷页面
|
||||
- [ ] `questionnaire/scoring.vue` - 主观题评分页面
|
||||
- [ ] `questionnaire/ranking.vue` - 成绩排名页面
|
||||
- [ ] `scale/index.vue` - 量表管理页面(需要修改,显示问卷)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术实现细节
|
||||
|
||||
### 1. 量表列表统一显示问卷
|
||||
|
||||
**后端修改**:
|
||||
```java
|
||||
// PsyScaleController.java
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(PsyScale scale, @RequestParam(required = false) Boolean includeQuestionnaire)
|
||||
{
|
||||
startPage();
|
||||
List<PsyScale> scaleList = scaleService.selectScaleList(scale);
|
||||
|
||||
// 如果需要包含问卷
|
||||
if (includeQuestionnaire != null && includeQuestionnaire) {
|
||||
List<PsyQuestionnaire> questionnaireList = questionnaireService.selectQuestionnaireList(...);
|
||||
// 转换为统一的Scale格式,添加sourceType标识
|
||||
// 合并到scaleList
|
||||
}
|
||||
|
||||
return getDataTable(scaleList);
|
||||
}
|
||||
```
|
||||
|
||||
**前端修改**:
|
||||
```javascript
|
||||
// scale/index.vue
|
||||
// 在表格中添加类型列
|
||||
<el-table-column label="类型" prop="sourceType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.sourceType === 'questionnaire'" type="warning">问卷</el-tag>
|
||||
<el-tag v-else type="primary">量表</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
### 2. 问卷答题流程
|
||||
|
||||
参考量表测评的实现:
|
||||
1. 开始问卷 → 创建`PsyQuestionnaireAnswer`记录
|
||||
2. 获取题目 → 根据`paper_type`获取题目列表
|
||||
3. 保存答案 → 保存到`PsyQuestionnaireAnswerDetail`
|
||||
4. 提交问卷 → 自动计分 + 更新排名
|
||||
|
||||
### 3. 自动计分逻辑
|
||||
|
||||
```java
|
||||
// 客观题计分
|
||||
private BigDecimal calculateObjectiveScore(PsyQuestionnaireItem item, AnswerDetailVO answer) {
|
||||
switch(item.getItemType()) {
|
||||
case "radio":
|
||||
// 单选:检查选项是否正确
|
||||
return checkOptionCorrect(item, answer.getOptionId()) ? item.getScore() : BigDecimal.ZERO;
|
||||
case "checkbox":
|
||||
// 多选:检查所有选项是否正确
|
||||
return calculateMultiChoiceScore(item, answer.getOptionIds());
|
||||
case "boolean":
|
||||
// 判断:检查选项是否正确
|
||||
return checkOptionCorrect(item, answer.getOptionId()) ? item.getScore() : BigDecimal.ZERO;
|
||||
case "input":
|
||||
// 填空:文本匹配
|
||||
return checkTextMatch(item, answer.getAnswerText()) ? item.getScore() : BigDecimal.ZERO;
|
||||
// ... 其他类型
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 排名计算
|
||||
|
||||
```sql
|
||||
-- 更新排名
|
||||
UPDATE psy_questionnaire_answer qa
|
||||
SET qa.rank = (
|
||||
SELECT COUNT(*) + 1
|
||||
FROM psy_questionnaire_answer qa2
|
||||
WHERE qa2.questionnaire_id = qa.questionnaire_id
|
||||
AND (
|
||||
qa2.total_score > qa.total_score
|
||||
OR (qa2.total_score = qa.total_score AND qa2.submit_time < qa.submit_time)
|
||||
)
|
||||
)
|
||||
WHERE qa.questionnaire_id = #{questionnaireId}
|
||||
AND qa.status = '1'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 实施步骤
|
||||
|
||||
1. ✅ **创建问卷答案详情表**(SQL)
|
||||
2. ⏳ **修改量表列表查询,包含问卷**(后端)
|
||||
3. ⏳ **修改量表管理页面,显示问卷**(前端)
|
||||
4. ⏳ **创建问卷答题功能**(后端+前端)
|
||||
5. ⏳ **实现自动计分功能**(后端)
|
||||
6. ⏳ **实现主观题评分功能**(后端+前端)
|
||||
7. ⏳ **实现成绩排名功能**(后端+前端)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. 问卷和量表的数据结构不同,需要统一转换
|
||||
2. 组卷方式(随机、手动、混合)需要在获取题目时实现
|
||||
3. 主观题评分需要权限控制
|
||||
4. 排名计算要考虑性能,可能需要定时任务
|
||||
5. 填空题的文本匹配需要考虑容错性
|
||||
|
||||
---
|
||||
|
||||
**创建时间**: 2025-01-XX
|
||||
**最后更新**: 2025-01-XX
|
||||
|
||||
Loading…
Reference in New Issue
Block a user