2025年11月12日

This commit is contained in:
xiao12feng 2025-11-12 15:25:47 +08:00
parent 6473c94e1c
commit 6805ed2981
95 changed files with 10266 additions and 3267 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "disabled"
}

View File

@ -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'
})
}

View 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'
})
}

View 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'
})
}

View 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'
})
}

View File

@ -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'
})
}

View File

@ -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'
})
}

View File

@ -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']
}
}

View File

@ -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))
}
)

View File

@ -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() {
// URLscaleId
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);
// URLscaleId
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);
// URLscaleId
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('您还没有被分配任何量表权限,请联系管理员');
}
// URLscaleId
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,
// profileIdAssessmentStartVO
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,
// profileIdAssessmentStartVO
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;
}
});
},

View File

@ -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 || "未知错误"));
});
}
}
}
};

View 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>

View File

@ -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

View 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>

View 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() {
// URLquestionnaireId
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>

View 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>

View File

@ -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("加载报告失败,请检查报告是否存在");
});
// reportIdsourceType
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("获取答题记录失败");
});
}
}
};

View File

@ -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;

View File

@ -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) {

View File

@ -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(() => {})
}
}
}

View File

@ -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) {

View File

@ -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("&nbsp;", " ")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("&amp;", "&")
.replace("&quot;", "\"")
.replace("&#39;", "'");
// 去除多余空白
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)
{
// 忽略写入错误
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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)
{
// 忽略写入错误
}
}
}
}

View File

@ -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:
# 从数据源开关/默认关闭

View File

@ -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时可以访问

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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

View File

@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;");
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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
/**
* 生成二维码
*

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 '%权限%';

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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接口满足系统集成需求

View File

@ -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": "新标签页"
}
}
]

View File

@ -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

Binary file not shown.

View 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格式
- ✅ 支持单个和批量导出
- ✅ 支持按查询条件导出

View 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`

View 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

View 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