量表导入功能实现

This commit is contained in:
xiao12feng 2025-11-07 12:07:24 +08:00
parent f4cef1779c
commit 6473c94e1c
37 changed files with 9475 additions and 93 deletions

4621
SCL90症状自评量表.json Normal file

File diff suppressed because it is too large Load Diff

21
pom.xml
View File

@ -28,6 +28,7 @@
<oshi.version>6.8.3</oshi.version>
<commons.io.version>2.19.0</commons.io.version>
<poi.version>4.1.2</poi.version>
<pdfbox.version>2.0.29</pdfbox.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<zxing.version>3.5.1</zxing.version>
@ -155,6 +156,26 @@
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- POI Word文档解析 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- PDF解析工具 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<!-- velocity代码生成使用模板 -->
<dependency>

View File

@ -19,6 +19,9 @@ export function getPermission(permissionId) {
// 根据用户ID获取用户有权限访问的量表ID列表
export function getUserScaleIds(userId) {
if (!userId || userId === 'undefined' || userId === 'null' || isNaN(userId)) {
return Promise.reject(new Error('用户ID无效'));
}
return request({
url: '/psychology/permission/user/' + userId + '/scales',
method: 'get'
@ -61,12 +64,15 @@ export function delPermission(permissionIds) {
// 批量分配用户量表权限
export function assignUserScales(userId, scaleIds) {
if (!userId || userId === 'undefined' || userId === 'null' || isNaN(userId)) {
return Promise.reject(new Error('用户ID无效'));
}
return request({
url: '/psychology/permission/assign',
method: 'post',
data: {
userId: userId,
scaleIds: scaleIds
scaleIds: scaleIds || []
}
})
}

View File

@ -43,3 +43,54 @@ export function delScale(scaleId) {
})
}
// 导入量表JSON格式
export function importScale(data) {
return request({
url: '/psychology/scale/importJson',
method: 'post',
data: data
})
}
// 导入量表(文件格式)
export function importScaleFile(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/psychology/scale/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 预览文档解析结果
export function previewDocument(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/psychology/scale/preview',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 提取文档文本
export function extractText(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/psychology/scale/extract-text',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}

View File

@ -190,7 +190,8 @@ export default {
},
/** 查看报告按钮操作 */
handleView(row) {
this.$router.push({ path: 'assessment/report', query: { assessmentId: row.assessmentId } });
// 使ID
this.$router.push({ path: '/psychology/report/detail', query: { assessmentId: row.assessmentId } });
},
/** 删除按钮操作 */
handleDelete(row) {

View File

@ -110,17 +110,36 @@ export default {
methods: {
/** 加载量表列表 */
loadScales() {
// ID
const userId = this.$store.getters.userId;
// ID
const userId = this.$store.getters.id;
const roles = this.$store.getters.roles || [];
// userId === 1 roles 'admin'
const isAdmin = userId === 1 || (roles && roles.includes('admin'));
//
if (userId === 1) {
if (isAdmin) {
//
listScale({ status: '0' }).then(response => {
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
}).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 => {
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
}).catch(error => {
console.error("加载量表列表失败:", error);
this.$message.error('加载量表列表失败,请稍后重试');
});
return;
}
import('@/api/psychology/permission').then(module => {
module.getUserScaleIds(userId).then(permissionResponse => {
const allowedScaleIds = permissionResponse.data || [];
@ -134,7 +153,13 @@ export default {
this.scaleList = response.rows.filter(scale =>
scale.itemCount > 0 && allowedScaleIds.includes(scale.scaleId)
);
}).catch(error => {
console.error("加载量表列表失败:", error);
this.$message.error('加载量表列表失败,请稍后重试');
});
}).catch(error => {
console.error("加载用户量表权限失败:", error);
this.$message.error('加载量表权限失败,请稍后重试');
});
});
}

View File

@ -11,6 +11,17 @@
</div>
</div>
<div class="action-buttons">
<el-dropdown v-if="isAdmin" @command="handleQuickFill" style="margin-right: 10px;">
<el-button type="info" icon="el-icon-s-promotion">
快速填充<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="first">填充第一个选项并提交</el-dropdown-item>
<el-dropdown-item command="middle">填充中间选项并提交</el-dropdown-item>
<el-dropdown-item command="last">填充最后一个选项并提交</el-dropdown-item>
<el-dropdown-item command="random">随机填充并提交</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button type="warning" @click="handlePause">暂停测评</el-button>
<el-button @click="handleExit">退出</el-button>
</div>
@ -122,6 +133,12 @@ export default {
return false;
}
return this.answeredCount === this.itemList.length;
},
//
isAdmin() {
const userId = this.$store.getters.id;
const roles = this.$store.getters.roles || [];
return userId === 1 || (roles && roles.includes('admin'));
}
},
created() {
@ -190,7 +207,14 @@ export default {
loadAllOptions() {
const promises = this.itemList.map(item => {
return listOption(item.itemId).then(response => {
this.$set(this.optionMap, item.itemId, response.data || []);
const options = response.data || [];
// sortOrder
options.sort((a, b) => {
const orderA = a.sortOrder || 0;
const orderB = b.sortOrder || 0;
return orderA - orderB;
});
this.$set(this.optionMap, item.itemId, options);
});
});
return Promise.all(promises);
@ -312,6 +336,152 @@ export default {
this.$modal.msgError(error.msg || "提交失败,请重试");
});
});
},
/** 快速填充所有题目并提交(管理员功能) */
handleQuickFill(command) {
if (!this.isAdmin) {
this.$modal.msgError("您没有权限执行此操作");
return;
}
//
const hasAllOptions = this.itemList.every(item => {
const options = this.optionMap[item.itemId];
return options && options.length > 0;
});
if (!hasAllOptions) {
this.$modal.msgWarning("正在加载题目选项,请稍候...");
//
this.loadAllOptions().then(() => {
this.performQuickFill(command);
}).catch(error => {
console.error('加载选项失败:', error);
this.$modal.msgError("加载题目选项失败,请重试");
});
} else {
this.performQuickFill(command);
}
},
/** 执行快速填充 */
performQuickFill(command) {
let optionStrategy = null;
let strategyName = '';
switch (command) {
case 'first':
optionStrategy = (options) => options.length > 0 ? options[0] : null;
strategyName = '第一个选项';
break;
case 'middle':
optionStrategy = (options) => {
if (options.length === 0) return null;
const middleIndex = Math.floor((options.length - 1) / 2);
return options[middleIndex];
};
strategyName = '中间选项';
break;
case 'last':
optionStrategy = (options) => options.length > 0 ? options[options.length - 1] : null;
strategyName = '最后一个选项';
break;
case 'random':
optionStrategy = (options) => {
if (options.length === 0) return null;
const randomIndex = Math.floor(Math.random() * options.length);
return options[randomIndex];
};
strategyName = '随机选项';
break;
default:
return;
}
this.$modal.confirm(`确定要用"${strategyName}"填充所有题目并提交吗?此操作不可撤销。`).then(() => {
this.loading = true;
this.$modal.loading("正在快速填充所有题目...");
//
const fillPromises = [];
for (const item of this.itemList) {
const options = this.optionMap[item.itemId] || [];
if (options.length === 0) {
console.warn(`题目 ${item.itemId} 没有选项,跳过`);
continue; //
}
if (item.itemType === 'single' || item.itemType === 'matrix') {
//
const selectedOption = optionStrategy(options);
if (selectedOption) {
const answer = {
assessmentId: this.assessmentId,
itemId: item.itemId,
optionId: selectedOption.optionId,
optionIds: null,
answerScore: selectedOption.optionScore || 0
};
this.$set(this.answersMap, item.itemId, answer);
fillPromises.push(this.saveAnswerToServerPromise(answer));
}
} else if (item.itemType === 'multiple') {
//
const selectedOption = optionStrategy(options);
if (selectedOption) {
const selectedOptions = [selectedOption.optionId];
const totalScore = selectedOption.optionScore || 0;
const answer = {
assessmentId: this.assessmentId,
itemId: item.itemId,
optionId: null,
optionIds: selectedOptions.join(','),
answerScore: totalScore
};
this.$set(this.answersMap, item.itemId, answer);
fillPromises.push(this.saveAnswerToServerPromise(answer));
}
}
}
if (fillPromises.length === 0) {
this.$modal.closeLoading();
this.loading = false;
this.$modal.msgError("没有可填充的题目");
return;
}
//
Promise.all(fillPromises).then(() => {
this.$modal.closeLoading();
this.$modal.msgSuccess(`已填充 ${fillPromises.length} 道题目,正在提交测评...`);
//
submitAssessment(this.assessmentId).then(response => {
this.loading = false;
this.$modal.msgSuccess(response.msg || "测评已提交,报告已生成");
this.$router.push('/psychology/assessment');
}).catch(error => {
this.loading = false;
this.$modal.msgError(error.msg || "提交失败,请重试");
});
}).catch(error => {
this.$modal.closeLoading();
this.loading = false;
console.error('快速填充失败:', error);
this.$modal.msgError("快速填充失败,请重试");
});
}).catch(() => {
//
});
},
/** 保存答案到服务器返回Promise */
saveAnswerToServerPromise(answer) {
return saveAnswer(answer).catch(error => {
console.error('保存答案失败:', error);
throw error;
});
}
},
watch: {

View File

@ -54,11 +54,17 @@ export default {
},
created() {
const userId = this.$route.params.userId;
if (userId) {
this.userId = parseInt(userId);
this.loadUserInfo();
this.loadScales();
this.loadUserScales();
if (userId && userId !== 'undefined' && userId !== 'null') {
const parsedUserId = parseInt(userId);
if (!isNaN(parsedUserId) && parsedUserId > 0) {
this.userId = parsedUserId;
this.loadUserInfo();
this.loadScales();
this.loadUserScales();
} else {
this.$modal.msgError("用户ID参数无效");
this.handleBack();
}
} else {
this.$modal.msgError("缺少用户ID参数");
this.handleBack();
@ -83,12 +89,23 @@ export default {
},
/** 加载用户已有权限的量表 */
loadUserScales() {
if (!this.userId || isNaN(this.userId)) {
this.$modal.msgError("用户ID无效无法加载权限");
return;
}
getUserScaleIds(this.userId).then(response => {
this.selectedScaleIds = response.data || [];
}).catch(error => {
console.error("加载用户量表权限失败:", error);
this.$modal.msgError("加载用户量表权限失败");
});
},
/** 提交表单 */
submitForm() {
if (!this.userId || isNaN(this.userId)) {
this.$modal.msgError("用户ID无效无法保存");
return;
}
this.loading = true;
assignUserScales(this.userId, this.selectedScaleIds).then(response => {
this.$modal.msgSuccess("分配成功");

View File

@ -54,6 +54,16 @@
v-hasPermi="['psychology:scale:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-upload2"
size="mini"
@click="handleImport"
v-hasPermi="['psychology:scale:add']"
>导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
@ -254,11 +264,68 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="importTitle" :visible.sync="importOpen" width="800px" append-to-body>
<el-alert
title="导入说明"
type="info"
:closable="false"
style="margin-bottom: 20px;"
>
<div slot="description">
<p>1. 支持的文件格式JSONPDFDOCXDOC</p>
<p>2. PDF和DOCX文件会自动解析题目选项等信息</p>
<p>3. 建议先使用"预览解析结果"查看识别效果确认无误后再导入</p>
<p>4. 请确保量表编码唯一否则导入将失败</p>
<p>5. 题目数量会自动计算无需手动填写</p>
</div>
</el-alert>
<el-tabs v-model="importTab" type="border-card">
<el-tab-pane label="JSON文本" name="text">
<el-input
type="textarea"
:rows="20"
v-model="importJsonText"
placeholder="请粘贴JSON格式的量表数据"
></el-input>
</el-tab-pane>
<el-tab-pane label="文件上传" name="file">
<el-upload
ref="upload"
:limit="1"
accept=".json,.pdf,.docx,.doc"
:disabled="upload.isUploading"
:auto-upload="false"
:on-change="handleFileChange"
drag
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">支持JSONPDFDOCXDOC格式文件系统会自动识别并解析</div>
</el-upload>
<div v-if="upload.selectedFile" style="margin-top: 10px;">
<el-tag type="info">已选择文件: {{ upload.selectedFile.name }}</el-tag>
<el-button
v-if="isDocumentFile(upload.selectedFile.name)"
type="text"
size="mini"
@click="handlePreview"
style="margin-left: 10px;"
>预览解析结果</el-button>
</div>
</el-tab-pane>
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitImport"> </el-button>
<el-button @click="cancelImport"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listScale, getScale, delScale, addScale, updateScale } from "@/api/psychology/scale"
import { listScale, getScale, delScale, addScale, updateScale, importScale, importScaleFile, previewDocument } from "@/api/psychology/scale"
export default {
name: "PsyScale",
@ -283,6 +350,19 @@ export default {
title: "",
//
open: false,
//
importOpen: false,
importTitle: "导入量表",
importTab: "text",
importJsonText: "",
upload: {
//
isUploading: false,
//
selectedFile: null,
//
fileContent: null
},
//
queryParams: {
pageNum: 1,
@ -450,6 +530,156 @@ export default {
handleManageFactors(row) {
const scaleId = row.scaleId
this.$router.push({ path: '/psychology/scale/factor', query: { scaleId: scaleId, scaleName: row.scaleName } })
},
/** 导入按钮操作 */
handleImport() {
this.importOpen = true
this.importTab = "text"
this.importJsonText = ""
},
/** 取消导入 */
cancelImport() {
this.importOpen = false
this.importJsonText = ""
this.upload.selectedFile = null
this.upload.fileContent = null
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
},
/** 提交导入 */
submitImport() {
if (this.importTab === "text") {
if (!this.importJsonText || this.importJsonText.trim() === "") {
this.$modal.msgWarning("请输入JSON数据")
return
}
try {
const importData = JSON.parse(this.importJsonText)
importScale(importData).then(response => {
this.$modal.msgSuccess(response.msg || "导入成功")
this.importOpen = false
this.importJsonText = ""
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
})
} catch (e) {
this.$modal.msgError("JSON格式错误" + e.message)
}
} else {
//
if (!this.upload.selectedFile) {
this.$modal.msgWarning("请先选择要上传的文件")
return
}
//
const fileName = this.upload.selectedFile.name || ""
if (this.isDocumentFile(fileName)) {
// PDF/DOCX/DOC
this.doImportFile()
} else {
// JSON
if (!this.upload.fileContent) {
this.$modal.msgWarning("请先选择要上传的文件")
return
}
try {
const importData = JSON.parse(this.upload.fileContent)
this.upload.isUploading = true
importScale(importData).then(response => {
this.$modal.msgSuccess(response.msg || "导入成功")
this.importOpen = false
this.importJsonText = ""
this.upload.selectedFile = null
this.upload.fileContent = null
this.$refs.upload.clearFiles()
this.upload.isUploading = false
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
this.upload.isUploading = false
})
} catch (e) {
this.$modal.msgError("JSON格式错误" + e.message)
this.upload.isUploading = false
}
}
}
},
/** 判断是否为文档文件PDF/DOCX/DOC */
isDocumentFile(fileName) {
if (!fileName) return false
const lowerName = fileName.toLowerCase()
return lowerName.endsWith('.pdf') || lowerName.endsWith('.docx') || lowerName.endsWith('.doc')
},
/** 直接导入文件(不预览) */
doImportFile() {
if (!this.upload.selectedFile) {
this.$modal.msgWarning("请先选择文件")
return
}
this.upload.isUploading = true
importScaleFile(this.upload.selectedFile).then(response => {
this.$modal.msgSuccess(response.msg || "导入成功")
this.importOpen = false
this.upload.selectedFile = null
this.upload.fileContent = null
this.$refs.upload.clearFiles()
this.upload.isUploading = false
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
this.upload.isUploading = false
})
},
/** 预览文档解析结果 */
handlePreview() {
if (!this.upload.selectedFile) {
this.$modal.msgWarning("请先选择文件")
return
}
this.$modal.loading("正在解析文档,请稍候...")
previewDocument(this.upload.selectedFile).then(response => {
this.$modal.closeLoading()
const data = response.data
if (data && data.importData) {
// JSON
this.importJsonText = JSON.stringify(data.importData, null, 2)
this.importTab = "text"
this.$modal.msgSuccess("文档解析完成,请检查并调整解析结果后再导入")
} else {
this.$modal.msgError("解析失败,未能识别量表结构")
}
}).catch(error => {
this.$modal.closeLoading()
this.$modal.msgError(error.msg || "解析失败")
})
},
/** 文件选择变化处理 */
handleFileChange(file, fileList) {
if (file.raw) {
this.upload.selectedFile = file.raw
const fileName = file.raw.name || ""
// JSON
if (fileName.toLowerCase().endsWith('.json')) {
const reader = new FileReader()
reader.onload = (e) => {
this.upload.fileContent = e.target.result
}
reader.onerror = () => {
this.$modal.msgError("文件读取失败")
this.upload.selectedFile = null
this.upload.fileContent = null
}
reader.readAsText(file.raw, 'UTF-8')
} else {
// PDF/DOCX/DOC
this.upload.fileContent = null
}
}
}
}
}

View File

@ -492,7 +492,16 @@ export default {
},
/** 分配量表权限操作 */
handleAuthScale: function(row) {
this.$router.push("/psychology/permission/user/" + row.userId)
if (!row || !row.userId) {
this.$modal.msgError("用户信息不完整,无法分配权限");
return;
}
const userId = row.userId;
if (isNaN(userId) || userId <= 0) {
this.$modal.msgError("用户ID无效");
return;
}
this.$router.push("/psychology/permission/user/" + userId);
},
/** 提交按钮 */
submitForm: function() {

View File

@ -72,6 +72,23 @@
<artifactId>javase</artifactId>
</dependency>
<!-- PDF解析 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
</dependency>
<!-- Word文档解析 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -258,14 +258,27 @@ public class PsyAssessmentController extends BaseController
assessmentService.updateAssessment(assessment);
// 自动生成报告
reportService.generateReport(assessmentId);
return success("提交成功,报告已生成");
try
{
reportService.generateReport(assessmentId);
return success("提交成功,报告已生成");
}
catch (Exception e)
{
// 报告生成失败但不影响测评提交
e.printStackTrace();
return success("提交成功,但报告生成失败:" + e.getMessage());
}
}
catch (RuntimeException e)
{
return error("提交失败:" + e.getMessage());
}
catch (Exception e)
{
e.printStackTrace();
return error("提交失败:" + e.getMessage());
}
}
}

View File

@ -11,13 +11,17 @@ 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 org.springframework.web.multipart.MultipartFile;
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.vo.ScaleImportVO;
import com.ddnai.system.service.psychology.IDocumentParseService;
import com.ddnai.system.service.psychology.IPsyScaleService;
/**
@ -31,6 +35,9 @@ public class PsyScaleController extends BaseController
{
@Autowired
private IPsyScaleService scaleService;
@Autowired
private IDocumentParseService documentParseService;
/**
* 获取量表列表
@ -92,5 +99,159 @@ public class PsyScaleController extends BaseController
{
return toAjax(scaleService.deleteScaleByIds(scaleIds));
}
/**
* 导入量表支持JSON和文档文件
*/
@PreAuthorize("@ss.hasPermi('psychology:scale:add')")
@Log(title = "心理量表", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult importScale(@RequestParam(value = "file", required = false) MultipartFile file,
@RequestParam(value = "jsonData", required = false) String jsonData)
{
try
{
ScaleImportVO data = null;
java.util.Map<String, Object> rawJsonData = null;
// 如果上传了文件尝试解析文档
if (file != null && !file.isEmpty())
{
String filename = file.getOriginalFilename();
if (documentParseService.isSupported(filename))
{
// 解析文档PDFDOCXDOC
data = documentParseService.parseDocument(file);
// PDF/DOCX解析的结果可能没有factorCode所以rawJsonData为null
}
else
{
// 不支持的文件格式尝试作为JSON文件读取
String jsonText = new String(file.getBytes(), java.nio.charset.StandardCharsets.UTF_8);
// 先解析为Map以保留factorCode信息
@SuppressWarnings("unchecked")
java.util.Map<String, Object> parsedMap = (java.util.Map<String, Object>)
com.alibaba.fastjson2.JSON.parseObject(jsonText, java.util.Map.class);
rawJsonData = parsedMap;
// 再转换为ScaleImportVO
data = com.alibaba.fastjson2.JSON.parseObject(jsonText, ScaleImportVO.class);
}
}
// 如果没有文件尝试从jsonData参数解析
else if (jsonData != null && !jsonData.trim().isEmpty())
{
// 先解析为Map以保留factorCode信息
@SuppressWarnings("unchecked")
java.util.Map<String, Object> parsedMap = (java.util.Map<String, Object>)
com.alibaba.fastjson2.JSON.parseObject(jsonData, java.util.Map.class);
rawJsonData = parsedMap;
// 再转换为ScaleImportVO
data = com.alibaba.fastjson2.JSON.parseObject(jsonData, ScaleImportVO.class);
}
if (data == null)
{
return error("导入数据不能为空请上传文件或提供JSON数据");
}
// 调用支持factorCode映射的导入方法
Long scaleId = scaleService.importScale(data, rawJsonData, getUsername());
return success("导入成功量表ID" + scaleId);
}
catch (Exception e)
{
return error("导入失败:" + e.getMessage());
}
}
/**
* 导入量表JSON格式用于前端直接传递JSON对象
*/
@PreAuthorize("@ss.hasPermi('psychology:scale:add')")
@Log(title = "心理量表", businessType = BusinessType.IMPORT)
@PostMapping("/importJson")
public AjaxResult importScaleJson(@RequestBody java.util.Map<String, Object> jsonData)
{
try
{
if (jsonData == null)
{
return error("导入数据不能为空");
}
// 使用ObjectMapper转换为ScaleImportVO
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
ScaleImportVO importData = objectMapper.convertValue(jsonData, ScaleImportVO.class);
// 调用导入服务传入原始JSON以便处理factorCode映射
Long scaleId = scaleService.importScale(importData, jsonData, getUsername());
return success("导入成功量表ID" + scaleId);
}
catch (Exception e)
{
return error("导入失败:" + e.getMessage());
}
}
/**
* 预览文档解析结果不导入仅返回解析后的数据
*/
@PreAuthorize("@ss.hasPermi('psychology:scale:add')")
@PostMapping("/preview")
public AjaxResult previewDocument(@RequestParam("file") MultipartFile file)
{
try
{
if (file == null || file.isEmpty())
{
return error("请选择要预览的文件");
}
String filename = file.getOriginalFilename();
if (!documentParseService.isSupported(filename))
{
return error("不支持的文件格式仅支持PDF、DOCX、DOC格式");
}
// 解析文档
ScaleImportVO importData = documentParseService.parseDocument(file);
// 同时返回原始文本方便用户查看和调整
String originalText = documentParseService.extractText(file);
java.util.Map<String, Object> result = new java.util.HashMap<>();
result.put("importData", importData);
result.put("originalText", originalText);
return success(result);
}
catch (Exception e)
{
return error("解析失败:" + e.getMessage());
}
}
/**
* 提取文档文本内容用于调试和查看
*/
@PreAuthorize("@ss.hasPermi('psychology:scale:add')")
@PostMapping("/extract-text")
public AjaxResult extractText(@RequestParam("file") MultipartFile file)
{
try
{
if (file == null || file.isEmpty())
{
return error("请选择文件");
}
String text = documentParseService.extractText(file);
return success(text);
}
catch (Exception e)
{
return error("提取文本失败:" + e.getMessage());
}
}
}

View File

@ -22,6 +22,29 @@
<groupId>com.ddnai</groupId>
<artifactId>ry-news-common</artifactId>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<!-- POI Word文档解析 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
</dependency>
<!-- PDF解析工具 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox-tools</artifactId>
</dependency>
</dependencies>

View File

@ -0,0 +1,54 @@
package com.ddnai.system.domain.psychology.vo;
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 解释配置导入VO支持factorCode映射
*
* @author ddnai
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class InterpretationImportVO extends PsyResultInterpretation
{
private static final long serialVersionUID = 1L;
/** 因子编码用于导入时映射到factorId */
@JsonProperty("factorCode")
private String factorCode;
public String getFactorCode()
{
return factorCode;
}
public void setFactorCode(String factorCode)
{
this.factorCode = factorCode;
}
/**
* 转换为PsyResultInterpretation对象
*/
public PsyResultInterpretation toPsyResultInterpretation()
{
PsyResultInterpretation interpretation = new PsyResultInterpretation();
interpretation.setScaleId(this.getScaleId());
interpretation.setFactorId(this.getFactorId());
interpretation.setScoreRangeMin(this.getScoreRangeMin());
interpretation.setScoreRangeMax(this.getScoreRangeMax());
interpretation.setLevel(this.getLevel());
interpretation.setLevelName(this.getLevelName());
interpretation.setInterpretationTitle(this.getInterpretationTitle());
interpretation.setInterpretationContent(this.getInterpretationContent());
interpretation.setSuggestions(this.getSuggestions());
interpretation.setSortOrder(this.getSortOrder());
interpretation.setCreateBy(this.getCreateBy());
interpretation.setCreateTime(this.getCreateTime());
interpretation.setUpdateBy(this.getUpdateBy());
interpretation.setUpdateTime(this.getUpdateTime());
return interpretation;
}
}

View File

@ -0,0 +1,183 @@
package com.ddnai.system.domain.psychology.vo;
import java.util.List;
import com.ddnai.system.domain.psychology.PsyScale;
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;
/**
* 量表导入VO
* 用于接收导入的JSON数据
*
* @author ddnai
*/
public class ScaleImportVO
{
/** 量表基本信息 */
private PsyScale scale;
/** 因子列表 */
private List<FactorImportVO> factors;
/** 题目列表 */
private List<ItemImportVO> items;
/** 解释配置列表(可选) */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.AS_EMPTY)
private List<PsyResultInterpretation> interpretations;
/** 预警规则列表(可选) */
@com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.AS_EMPTY)
private List<PsyWarningRule> warningRules;
public PsyScale getScale()
{
return scale;
}
public void setScale(PsyScale scale)
{
this.scale = scale;
}
public List<FactorImportVO> getFactors()
{
return factors;
}
public void setFactors(List<FactorImportVO> factors)
{
this.factors = factors;
}
public List<ItemImportVO> getItems()
{
return items;
}
public void setItems(List<ItemImportVO> items)
{
this.items = items;
}
public List<PsyResultInterpretation> getInterpretations()
{
return interpretations;
}
public void setInterpretations(List<PsyResultInterpretation> interpretations)
{
this.interpretations = interpretations;
}
public List<PsyWarningRule> getWarningRules()
{
return warningRules;
}
public void setWarningRules(List<PsyWarningRule> warningRules)
{
this.warningRules = warningRules;
}
/**
* 因子导入VO包含计分规则
*/
public static class FactorImportVO
{
/** 因子信息 */
private PsyFactor factor;
/** 计分规则列表 */
private List<FactorRuleImportVO> rules;
public PsyFactor getFactor()
{
return factor;
}
public void setFactor(PsyFactor factor)
{
this.factor = factor;
}
public List<FactorRuleImportVO> getRules()
{
return rules;
}
public void setRules(List<FactorRuleImportVO> rules)
{
this.rules = rules;
}
}
/**
* 因子计分规则导入VO包含题目序号用于映射
*/
public static class FactorRuleImportVO
{
/** 题目序号用于映射到新的题目ID */
private Integer itemNumber;
/** 计分规则信息 */
private PsyFactorRule rule;
public Integer getItemNumber()
{
return itemNumber;
}
public void setItemNumber(Integer itemNumber)
{
this.itemNumber = itemNumber;
}
public PsyFactorRule getRule()
{
return rule;
}
public void setRule(PsyFactorRule rule)
{
this.rule = rule;
}
}
/**
* 题目导入VO包含选项
*/
public static class ItemImportVO
{
/** 题目信息 */
private PsyScaleItem item;
/** 选项列表 */
private List<PsyScaleOption> options;
public PsyScaleItem getItem()
{
return item;
}
public void setItem(PsyScaleItem item)
{
this.item = item;
}
public List<PsyScaleOption> getOptions()
{
return options;
}
public void setOptions(List<PsyScaleOption> options)
{
this.options = options;
}
}
}

View File

@ -0,0 +1,55 @@
package com.ddnai.system.domain.psychology.vo;
import com.ddnai.system.domain.psychology.PsyWarningRule;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 预警规则导入VO支持factorCode映射
*
* @author ddnai
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class WarningRuleImportVO extends PsyWarningRule
{
private static final long serialVersionUID = 1L;
/** 因子编码用于导入时映射到factorId */
@JsonProperty("factorCode")
private String factorCode;
public String getFactorCode()
{
return factorCode;
}
public void setFactorCode(String factorCode)
{
this.factorCode = factorCode;
}
/**
* 转换为PsyWarningRule对象
*/
public PsyWarningRule toPsyWarningRule()
{
PsyWarningRule warningRule = new PsyWarningRule();
warningRule.setScaleId(this.getScaleId());
warningRule.setFactorId(this.getFactorId());
warningRule.setRuleName(this.getRuleName());
warningRule.setWarningLevel(this.getWarningLevel());
warningRule.setScoreMin(this.getScoreMin());
warningRule.setScoreMax(this.getScoreMax());
warningRule.setPercentileMin(this.getPercentileMin());
warningRule.setPercentileMax(this.getPercentileMax());
warningRule.setAutoRelief(this.getAutoRelief());
warningRule.setReliefCondition(this.getReliefCondition());
warningRule.setStatus(this.getStatus());
warningRule.setCreateBy(this.getCreateBy());
warningRule.setCreateTime(this.getCreateTime());
warningRule.setUpdateBy(this.getUpdateBy());
warningRule.setUpdateTime(this.getUpdateTime());
return warningRule;
}
}

View File

@ -89,5 +89,21 @@ public interface PsyAssessmentMapper
* @return 结果
*/
public int resumeAssessment(PsyAssessment assessment);
/**
* 根据量表ID查询测评记录数量
*
* @param scaleId 量表ID
* @return 测评记录数量
*/
public int countAssessmentByScaleId(Long scaleId);
/**
* 根据量表ID批量删除测评记录
*
* @param scaleIds 量表ID数组
* @return 删除数量
*/
public int deleteAssessmentByScaleIds(Long[] scaleIds);
}

View File

@ -73,5 +73,13 @@ public interface PsyScaleMapper
* @return 结果
*/
public int checkScaleCodeUnique(String scaleCode);
/**
* 检查量表是否被测评记录使用
*
* @param scaleId 量表ID
* @return 测评记录数量
*/
public int countAssessmentByScaleId(Long scaleId);
}

View File

@ -0,0 +1,893 @@
package com.ddnai.system.service.impl.psychology;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.ddnai.system.domain.psychology.PsyFactor;
import com.ddnai.system.domain.psychology.PsyFactorRule;
import com.ddnai.system.domain.psychology.PsyScale;
import com.ddnai.system.domain.psychology.PsyScaleItem;
import com.ddnai.system.domain.psychology.PsyScaleOption;
import com.ddnai.system.domain.psychology.vo.ScaleImportVO;
import com.ddnai.system.service.psychology.IDocumentParseService;
/**
* 文档解析服务实现
*
* @author ddnai
*/
@Service
public class DocumentParseServiceImpl implements IDocumentParseService
{
private static final Logger log = LoggerFactory.getLogger(DocumentParseServiceImpl.class);
// 题目识别正则匹配 "1." "1、" "(1)" "1 " 开头的题目
private static final Pattern ITEM_PATTERN = Pattern.compile("^\\s*(\\d+)[.、)\\s]\\s*(.+?)(?=\\s*\\d+[.、)\\s]|$)", Pattern.MULTILINE | Pattern.DOTALL);
// 选项识别正则匹配多种格式的选项
// 支持A. 没有 B. 很轻 A没有 B很轻 (A)没有 (B)很轻 没有 很轻
private static final Pattern OPTION_PATTERN = Pattern.compile(
"(?:^|\\s)([A-E①②③④⑤⑥⑦⑧⑨⑩]|[一二三四五六七八九十]|\\d+)[.、)\\s]\\s*([^A-E①②③④⑤⑥⑦⑧⑨⑩一二三四五六七八九十\\d.、\\)]+?)(?=\\s*[A-E①②③④⑤⑥⑦⑧⑨⑩一二三四五六七八九十\\d][.、)\\s]|\\s*$|\\r?\\n)",
Pattern.MULTILINE | Pattern.DOTALL
);
// SCL-90常见选项模式5级评分
private static final String[] SCL90_OPTIONS = {"没有", "很轻", "中等", "偏重", "严重"};
// 因子识别正则匹配因子名称支持多种格式
private static final Pattern FACTOR_PATTERN = Pattern.compile(
"(?:因子|维度|因素|F[\\s]*[\\d一二三四五六七八九十])[\\s]*(?:\\d+|一|二|三|四|五|六|七|八|九|十)[:\\s]*([^\\r\\n]+?)(?:\\r?\\n|$)",
Pattern.MULTILINE
);
// SCL-90因子定义
private static final Map<String, String> SCL90_FACTORS = new HashMap<>();
static {
SCL90_FACTORS.put("躯体化", "F1");
SCL90_FACTORS.put("强迫", "F2");
SCL90_FACTORS.put("人际关系", "F3");
SCL90_FACTORS.put("抑郁", "F4");
SCL90_FACTORS.put("焦虑", "F5");
SCL90_FACTORS.put("敌对", "F6");
SCL90_FACTORS.put("恐怖", "F7");
SCL90_FACTORS.put("偏执", "F8");
SCL90_FACTORS.put("精神病性", "F9");
}
@Override
public boolean isSupported(String filename)
{
if (StringUtils.isBlank(filename))
{
return false;
}
String lowerName = filename.toLowerCase();
return lowerName.endsWith(".pdf") || lowerName.endsWith(".docx") || lowerName.endsWith(".doc");
}
@Override
public String extractText(MultipartFile file) throws Exception
{
String filename = file.getOriginalFilename();
if (filename == null)
{
throw new IllegalArgumentException("文件名不能为空");
}
String lowerName = filename.toLowerCase();
InputStream inputStream = file.getInputStream();
try
{
if (lowerName.endsWith(".pdf"))
{
return extractTextFromPdf(inputStream);
}
else if (lowerName.endsWith(".docx"))
{
return extractTextFromDocx(inputStream);
}
else if (lowerName.endsWith(".doc"))
{
return extractTextFromDoc(inputStream);
}
else
{
throw new IllegalArgumentException("不支持的文件格式:" + filename);
}
}
finally
{
if (inputStream != null)
{
inputStream.close();
}
}
}
@Override
public ScaleImportVO parseDocument(MultipartFile file) throws Exception
{
log.info("开始解析文档:{}", file.getOriginalFilename());
String text = extractText(file);
log.debug("提取文本长度:{} 字符", text.length());
if (StringUtils.isBlank(text))
{
throw new RuntimeException("无法从文档中提取文本内容,请检查文档格式");
}
ScaleImportVO result = parseTextToScale(text, file.getOriginalFilename());
log.info("解析完成 - 量表:{},题目数:{},因子数:{}",
result.getScale().getScaleName(),
result.getItems() != null ? result.getItems().size() : 0,
result.getFactors() != null ? result.getFactors().size() : 0);
return result;
}
/**
* 从PDF提取文本
*/
private String extractTextFromPdf(InputStream inputStream) throws Exception
{
try (PDDocument document = PDDocument.load(inputStream))
{
PDFTextStripper stripper = new PDFTextStripper();
stripper.setStartPage(1);
stripper.setEndPage(document.getNumberOfPages());
return stripper.getText(document);
}
}
/**
* 从DOCX提取文本
*/
private String extractTextFromDocx(InputStream inputStream) throws Exception
{
XWPFDocument document = new XWPFDocument(inputStream);
try
{
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
try
{
return extractor.getText();
}
finally
{
extractor.close();
}
}
finally
{
document.close();
}
}
/**
* 从DOC提取文本
*/
private String extractTextFromDoc(InputStream inputStream) throws Exception
{
try (HWPFDocument document = new HWPFDocument(inputStream))
{
WordExtractor extractor = new WordExtractor(document);
return extractor.getText();
}
}
/**
* 将文本解析为量表数据结构
*/
private ScaleImportVO parseTextToScale(String text, String filename)
{
ScaleImportVO importVO = new ScaleImportVO();
// 检查是否为SCL-90量表
boolean isSCL90 = isSCL90Scale(text, filename);
// 1. 创建量表基本信息
PsyScale scale = new PsyScale();
scale.setScaleName(extractScaleName(text, filename));
scale.setScaleCode(generateScaleCode(filename));
scale.setScaleType(isSCL90 ? "symptom" : "general"); // SCL-90属于症状评估类
scale.setStatus("0"); // 默认正常状态
scale.setItemCount(null); // 由系统自动计算
importVO.setScale(scale);
// 2. 解析题目
List<ScaleImportVO.ItemImportVO> items;
if (isSCL90)
{
items = parseSCL90Items(text);
}
else
{
items = parseItems(text);
}
importVO.setItems(items);
// 3. 解析因子
List<ScaleImportVO.FactorImportVO> factors;
if (isSCL90)
{
factors = parseSCL90Factors(text, items);
}
else
{
factors = parseFactors(text, items);
}
importVO.setFactors(factors);
return importVO;
}
/**
* 判断是否为SCL-90量表
*/
private boolean isSCL90Scale(String text, String filename)
{
String lowerText = text.toLowerCase();
String lowerFilename = filename != null ? filename.toLowerCase() : "";
// 检查文件名和文本中是否包含SCL-90相关关键词
return lowerFilename.contains("scl") ||
lowerFilename.contains("scl-90") ||
lowerFilename.contains("scl90") ||
lowerText.contains("scl-90") ||
lowerText.contains("scl90") ||
lowerText.contains("症状自评量表") ||
(lowerText.contains("90") && lowerText.contains("症状"));
}
/**
* 提取量表名称
*/
private String extractScaleName(String text, String filename)
{
// 尝试从文件名提取
if (filename != null)
{
String name = filename.replaceAll("\\.(pdf|docx|doc)$", "").trim();
if (StringUtils.isNotBlank(name))
{
return name;
}
}
// 尝试从文本第一行提取
String[] lines = text.split("\\r?\\n");
for (String line : lines)
{
line = line.trim();
if (StringUtils.isNotBlank(line) && line.length() > 2 && line.length() < 100)
{
// 跳过常见的标题行
if (!line.matches("^第[一二三四五六七八九十]+[章节部分].*$")
&& !line.matches("^\\d+[.、].*$"))
{
return line;
}
}
}
return "导入的量表";
}
/**
* 生成量表编码
*/
private String generateScaleCode(String filename)
{
if (filename != null)
{
String code = filename.replaceAll("\\.(pdf|docx|doc)$", "")
.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", "")
.trim();
if (StringUtils.isNotBlank(code) && code.length() <= 50)
{
return code;
}
}
return "SCALE_" + System.currentTimeMillis();
}
/**
* 解析SCL-90题目列表特殊处理
*/
private List<ScaleImportVO.ItemImportVO> parseSCL90Items(String text)
{
List<ScaleImportVO.ItemImportVO> items = new ArrayList<>();
// SCL-90通常有90个题目使用更精确的正则匹配
// 匹配模式数字 + /空格 + 题目内容可能包含换行
Pattern scl90ItemPattern = Pattern.compile(
"^\\s*(\\d+)[.、]?\\s+([^\\d]+?)(?=\\s*\\d+[.、]|\\s*指导语|\\s*说明|$)",
Pattern.MULTILINE | Pattern.DOTALL
);
Matcher itemMatcher = scl90ItemPattern.matcher(text);
Map<Integer, String> itemMap = new HashMap<>();
while (itemMatcher.find())
{
try
{
int itemNum = Integer.parseInt(itemMatcher.group(1));
String content = itemMatcher.group(2).trim();
// 过滤掉明显不是题目的内容
if (content.length() < 3 || content.length() > 500)
{
continue;
}
// 跳过指导语说明等
if (content.contains("指导语") || content.contains("说明") ||
content.contains("评分标准") || content.contains("注意事项"))
{
continue;
}
itemMap.put(itemNum, content);
}
catch (Exception e)
{
log.debug("解析题目失败:{}", e.getMessage());
continue;
}
}
log.info("识别到 {} 个SCL-90题目", itemMap.size());
// 按题目序号排序并创建题目对象
itemMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
try
{
int itemNumber = entry.getKey();
String content = entry.getValue();
// 提取题目内容去掉选项部分
String itemContent = extractItemContent(content);
// 创建题目
PsyScaleItem item = new PsyScaleItem();
item.setItemNumber(itemNumber);
item.setItemContent(itemContent);
item.setItemType("single");
item.setRequired("1");
item.setReverseScore("0");
item.setSortOrder(itemNumber);
// 解析选项SCL-90使用5级评分
List<PsyScaleOption> options = parseSCL90Options(content);
// 如果没有解析到选项使用默认的5级选项
if (options.isEmpty())
{
options = createDefaultSCL90Options();
}
ScaleImportVO.ItemImportVO itemVO = new ScaleImportVO.ItemImportVO();
itemVO.setItem(item);
itemVO.setOptions(options);
items.add(itemVO);
}
catch (Exception e)
{
log.warn("创建题目失败,题目序号:{},错误:{}", entry.getKey(), e.getMessage());
}
});
return items;
}
/**
* 解析题目列表通用方法
*/
private List<ScaleImportVO.ItemImportVO> parseItems(String text)
{
List<ScaleImportVO.ItemImportVO> items = new ArrayList<>();
Matcher itemMatcher = ITEM_PATTERN.matcher(text);
int itemNumber = 1;
Map<Integer, String> itemMap = new HashMap<>();
while (itemMatcher.find())
{
try
{
int num = Integer.parseInt(itemMatcher.group(1));
String content = itemMatcher.group(2).trim();
// 过滤无效内容
if (content.length() < 3 || content.length() > 500)
{
continue;
}
itemMap.put(num, content);
}
catch (Exception e)
{
log.debug("解析题目失败:{}", e.getMessage());
continue;
}
}
log.info("识别到 {} 个题目", itemMap.size());
// 按题目序号排序并创建题目对象
itemMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
try
{
int num = entry.getKey();
String content = entry.getValue();
// 提取题目内容
String itemContent = extractItemContent(content);
// 创建题目
PsyScaleItem item = new PsyScaleItem();
item.setItemNumber(num);
item.setItemContent(itemContent);
item.setItemType("single");
item.setRequired("1");
item.setReverseScore("0");
item.setSortOrder(num);
// 解析选项
List<PsyScaleOption> options = parseOptions(content);
ScaleImportVO.ItemImportVO itemVO = new ScaleImportVO.ItemImportVO();
itemVO.setItem(item);
itemVO.setOptions(options);
items.add(itemVO);
}
catch (Exception e)
{
log.warn("创建题目失败,题目序号:{},错误:{}", entry.getKey(), e.getMessage());
}
});
return items;
}
/**
* 创建SCL-90默认的5级选项
*/
private List<PsyScaleOption> createDefaultSCL90Options()
{
List<PsyScaleOption> options = new ArrayList<>();
String[] optionTexts = {"没有", "很轻", "中等", "偏重", "严重"};
String[] optionCodes = {"A", "B", "C", "D", "E"};
for (int i = 0; i < optionTexts.length; i++)
{
PsyScaleOption option = new PsyScaleOption();
option.setOptionCode(optionCodes[i]);
option.setOptionContent(optionTexts[i]);
option.setOptionScore(BigDecimal.valueOf(i)); // 0-4分
option.setSortOrder(i + 1);
options.add(option);
}
return options;
}
/**
* 解析SCL-90选项5级评分
*/
private List<PsyScaleOption> parseSCL90Options(String text)
{
List<PsyScaleOption> options = new ArrayList<>();
// 尝试识别SCL-90的5个选项
for (int i = 0; i < SCL90_OPTIONS.length; i++)
{
String optionText = SCL90_OPTIONS[i];
if (text.contains(optionText))
{
PsyScaleOption option = new PsyScaleOption();
option.setOptionCode(String.valueOf((char)('A' + i)));
option.setOptionContent(optionText);
option.setOptionScore(BigDecimal.valueOf(i)); // 0-4分
option.setSortOrder(i + 1);
options.add(option);
}
}
// 如果没有识别到使用默认选项
if (options.isEmpty())
{
return createDefaultSCL90Options();
}
return options;
}
/**
* 提取题目内容去掉选项部分
*/
private String extractItemContent(String fullText)
{
// 找到第一个选项的位置
Matcher optionMatcher = OPTION_PATTERN.matcher(fullText);
if (optionMatcher.find())
{
int optionStart = optionMatcher.start();
return fullText.substring(0, optionStart).trim();
}
return fullText.trim();
}
/**
* 解析选项通用方法
*/
private List<PsyScaleOption> parseOptions(String text)
{
List<PsyScaleOption> options = new ArrayList<>();
// 方法1使用正则表达式匹配
Matcher optionMatcher = OPTION_PATTERN.matcher(text);
Map<Integer, PsyScaleOption> optionMap = new HashMap<>();
while (optionMatcher.find())
{
try
{
String optionCode = optionMatcher.group(1).trim();
String optionContent = optionMatcher.group(2).trim();
if (optionContent.length() < 1 || optionContent.length() > 100)
{
continue;
}
// 转换选项编码如果是中文数字或特殊字符
String code = convertOptionCode(optionCode);
int order = getOptionOrder(code);
PsyScaleOption option = new PsyScaleOption();
option.setOptionCode(code);
option.setOptionContent(optionContent);
// 尝试从选项中提取分值如果选项内容包含分数
BigDecimal score = extractScoreFromOption(optionContent, order);
option.setOptionScore(score);
option.setSortOrder(order);
optionMap.put(order, option);
}
catch (Exception e)
{
log.debug("解析选项失败:{}", e.getMessage());
continue;
}
}
// 如果正则匹配失败尝试识别常见的选项模式
if (optionMap.isEmpty())
{
optionMap = tryParseCommonOptions(text);
}
// 转换为列表并按顺序排序
options.addAll(optionMap.values());
options.sort((a, b) -> Integer.compare(a.getSortOrder(), b.getSortOrder()));
return options;
}
/**
* 转换选项编码
*/
private String convertOptionCode(String code)
{
if (code.matches("[A-Z]"))
{
return code;
}
else if (code.matches("\\d+"))
{
int num = Integer.parseInt(code);
return String.valueOf((char)('A' + num - 1));
}
else
{
// 中文数字或其他字符转换为A-Z
Map<String, String> codeMap = new HashMap<>();
codeMap.put("", "A"); codeMap.put("", "B"); codeMap.put("", "C");
codeMap.put("", "D"); codeMap.put("", "E"); codeMap.put("", "F");
codeMap.put("", "A"); codeMap.put("", "B"); codeMap.put("", "C");
codeMap.put("", "D"); codeMap.put("", "E");
return codeMap.getOrDefault(code, "A");
}
}
/**
* 获取选项顺序
*/
private int getOptionOrder(String code)
{
if (code != null && code.length() == 1 && code.matches("[A-Z]"))
{
return code.charAt(0) - 'A' + 1;
}
return 1;
}
/**
* 从选项内容中提取分值
*/
private BigDecimal extractScoreFromOption(String content, int defaultScore)
{
// 尝试从括号中提取分值 "没有(0分)" "A. 选项(1)"
Pattern scorePattern = Pattern.compile("[(](\\d+(?:\\.\\d+)?)[分)]?");
Matcher scoreMatcher = scorePattern.matcher(content);
if (scoreMatcher.find())
{
try
{
return BigDecimal.valueOf(Double.parseDouble(scoreMatcher.group(1)));
}
catch (NumberFormatException e)
{
// 忽略
}
}
// 默认使用顺序作为分值
return BigDecimal.valueOf(defaultScore - 1);
}
/**
* 尝试解析常见选项模式
*/
private Map<Integer, PsyScaleOption> tryParseCommonOptions(String text)
{
Map<Integer, PsyScaleOption> options = new HashMap<>();
// 常见的选项模式
String[] commonPatterns = {
"没有|很轻|中等|偏重|严重",
"是|否",
"同意|不同意|不确定",
"非常同意|同意|不确定|不同意|非常不同意"
};
for (String pattern : commonPatterns)
{
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(text);
int order = 1;
while (m.find() && order <= 5)
{
String content = m.group().trim();
if (!content.isEmpty())
{
PsyScaleOption option = new PsyScaleOption();
option.setOptionCode(String.valueOf((char)('A' + order - 1)));
option.setOptionContent(content);
option.setOptionScore(BigDecimal.valueOf(order - 1));
option.setSortOrder(order);
options.put(order, option);
order++;
}
}
if (!options.isEmpty())
{
break;
}
}
return options;
}
/**
* 解析SCL-90因子9个因子
*/
private List<ScaleImportVO.FactorImportVO> parseSCL90Factors(String text, List<ScaleImportVO.ItemImportVO> items)
{
List<ScaleImportVO.FactorImportVO> factors = new ArrayList<>();
// SCL-90的9个因子定义
String[][] scl90FactorDefs = {
{"F1", "躯体化", "1,4,12,27,40,42,48,49,52,53,56,58"},
{"F2", "强迫症状", "3,9,10,28,38,45,46,51,55,65"},
{"F3", "人际关系敏感", "6,21,34,36,37,41,61,69,73"},
{"F4", "抑郁", "5,14,15,20,22,26,29,30,31,32,54,71,79"},
{"F5", "焦虑", "2,17,23,33,39,57,72,78,80,86"},
{"F6", "敌对", "11,24,63,67,74,81"},
{"F7", "恐怖", "13,25,47,50,70,75,82"},
{"F8", "偏执", "8,18,43,68,76,83"},
{"F9", "精神病性", "7,16,35,62,77,84,85,87,88,90"}
};
// 尝试从文本中识别因子名称
Map<String, String> factorNameMap = new HashMap<>();
for (String[] def : scl90FactorDefs)
{
factorNameMap.put(def[0], def[1]);
// 尝试在文本中查找因子名称
String factorName = def[1];
Pattern namePattern = Pattern.compile(
"(?:因子|维度|因素|F\\s*\\d+)[^:]*[:]?\\s*" + factorName,
Pattern.CASE_INSENSITIVE
);
Matcher nameMatcher = namePattern.matcher(text);
if (nameMatcher.find())
{
// 如果找到使用找到的名称
String foundName = nameMatcher.group();
if (foundName.length() > factorName.length())
{
factorNameMap.put(def[0], factorName); // 保持简洁名称
}
}
}
// 创建因子对象
for (int i = 0; i < scl90FactorDefs.length; i++)
{
String[] def = scl90FactorDefs[i];
String factorCode = def[0];
String factorName = factorNameMap.get(factorCode);
PsyFactor factor = new PsyFactor();
factor.setFactorCode(factorCode);
factor.setFactorName(factorName);
factor.setFactorOrder(i + 1);
factor.setFactorDescription("SCL-90 " + factorName + "因子");
// 解析题目编号并创建计分规则这里简化处理实际需要根据题目序号映射
List<ScaleImportVO.FactorRuleImportVO> rules = new ArrayList<>();
String[] itemNumbers = def[2].split(",");
for (String itemNumStr : itemNumbers)
{
try
{
Integer itemNumber = Integer.parseInt(itemNumStr.trim());
ScaleImportVO.FactorRuleImportVO ruleVO = new ScaleImportVO.FactorRuleImportVO();
ruleVO.setItemNumber(itemNumber);
// 创建默认的计分规则简单求和
PsyFactorRule rule = new PsyFactorRule();
rule.setWeight(BigDecimal.ONE); // 权重为1
rule.setCalculationType("sum"); // 求和
ruleVO.setRule(rule);
rules.add(ruleVO);
}
catch (NumberFormatException e)
{
log.debug("解析题目编号失败:{}", itemNumStr);
}
}
ScaleImportVO.FactorImportVO factorVO = new ScaleImportVO.FactorImportVO();
factorVO.setFactor(factor);
factorVO.setRules(rules);
factors.add(factorVO);
}
log.info("解析SCL-90因子完成共 {} 个因子", factors.size());
return factors;
}
/**
* 解析因子通用方法
*/
private List<ScaleImportVO.FactorImportVO> parseFactors(String text, List<ScaleImportVO.ItemImportVO> items)
{
List<ScaleImportVO.FactorImportVO> factors = new ArrayList<>();
// 使用正则表达式匹配因子
Matcher factorMatcher = FACTOR_PATTERN.matcher(text);
Map<Integer, String> factorMap = new HashMap<>();
while (factorMatcher.find())
{
try
{
String factorName = factorMatcher.group(1).trim();
// 尝试提取因子序号
int factorOrder = extractFactorOrder(factorName, factorMap.size() + 1);
factorMap.put(factorOrder, factorName);
}
catch (Exception e)
{
log.debug("解析因子失败:{}", e.getMessage());
continue;
}
}
log.info("识别到 {} 个因子", factorMap.size());
// 创建因子对象
factorMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> {
try
{
int order = entry.getKey();
String name = entry.getValue();
PsyFactor factor = new PsyFactor();
factor.setFactorCode("F" + order);
factor.setFactorName(name);
factor.setFactorOrder(order);
ScaleImportVO.FactorImportVO factorVO = new ScaleImportVO.FactorImportVO();
factorVO.setFactor(factor);
factorVO.setRules(new ArrayList<>()); // 计分规则需要手动配置
factors.add(factorVO);
}
catch (Exception e)
{
log.warn("创建因子失败,因子序号:{},错误:{}", entry.getKey(), e.getMessage());
}
});
return factors;
}
/**
* 提取因子序号
*/
private int extractFactorOrder(String factorName, int defaultOrder)
{
// 尝试从因子名称中提取数字
Pattern numPattern = Pattern.compile("\\d+");
Matcher numMatcher = numPattern.matcher(factorName);
if (numMatcher.find())
{
try
{
return Integer.parseInt(numMatcher.group());
}
catch (NumberFormatException e)
{
// 忽略
}
}
return defaultOrder;
}
}

View File

@ -1,11 +1,11 @@
package com.ddnai.system.service.impl.psychology;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -116,9 +116,17 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
// 2. 获取量表信息
PsyScale scale = scaleService.selectScaleById(assessment.getScaleId());
if (scale == null)
{
throw new RuntimeException("量表不存在");
}
// 3. 获取所有答案
List<PsyAssessmentAnswer> answers = answerService.selectAnswerListByAssessmentId(assessmentId);
if (answers == null || answers.isEmpty())
{
throw new RuntimeException("测评答案为空,无法生成报告");
}
// 4. 获取因子列表和计分规则
List<PsyFactor> factors = factorService.selectFactorListByScaleId(assessment.getScaleId());
@ -137,18 +145,25 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
List<PsyFactorRule> rules = factorRulesMap.get(factor.getFactorId());
if (rules == null || rules.isEmpty())
{
System.out.println("因子 " + factor.getFactorName() + " (ID: " + factor.getFactorId() + ") 没有计分规则,跳过");
continue;
}
BigDecimal factorScore = calculateFactorScore(factor, rules, answers);
totalScore = totalScore.add(factorScore);
if (factorScore.compareTo(BigDecimal.ZERO) > 0 || rules.stream().anyMatch(rule -> {
PsyAssessmentAnswer answer = findAnswerByItemId(answers, rule.getItemId());
return answer != null;
}))
{
totalScore = totalScore.add(factorScore);
PsyFactorScore score = new PsyFactorScore();
score.setAssessmentId(assessmentId);
score.setFactorId(factor.getFactorId());
score.setFactorScore(factorScore);
score.setCreateTime(DateUtils.getNowDate());
factorScores.add(score);
PsyFactorScore score = new PsyFactorScore();
score.setAssessmentId(assessmentId);
score.setFactorId(factor.getFactorId());
score.setFactorScore(factorScore);
score.setCreateTime(DateUtils.getNowDate());
factorScores.add(score);
}
}
// 6. 批量保存因子得分
@ -159,6 +174,10 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
// 保存新得分
factorScoreService.batchInsertFactorScore(factorScores);
}
else
{
System.out.println("警告:没有计算出任何因子得分,答案数量: " + answers.size() + ", 因子数量: " + factors.size());
}
// 7. 更新测评总分
assessment.setTotalScore(totalScore);
@ -166,23 +185,45 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
assessmentService.updateAssessment(assessment);
// 8. 生成报告内容
String reportContent = generateReportContent(scale, factors, factorScores, assessment);
String reportContent = generateReportContent(scale, factors, factorScores, assessment, answers);
// 9. 生成报告摘要
String summary = generateSummary(factors, factorScores, assessment);
String summary = generateSummary(factors, factorScores, assessment, answers);
// 10. 保存报告
PsyAssessmentReport report = new PsyAssessmentReport();
report.setAssessmentId(assessmentId);
report.setReportType("standard");
report.setReportTitle(scale.getScaleName() + "测评报告");
report.setReportContent(reportContent);
report.setSummary(summary);
report.setIsGenerated("1");
report.setGenerateTime(DateUtils.getNowDate());
report.setCreateBy("system");
report.setCreateTime(DateUtils.getNowDate());
reportMapper.insertReport(report);
// 10. 检查是否已存在报告如果存在则更新否则新建
PsyAssessmentReport existingReport = reportMapper.selectReportByAssessmentId(assessmentId);
PsyAssessmentReport report;
if (existingReport != null)
{
// 更新现有报告
report = existingReport;
report.setReportType("standard");
report.setReportTitle(scale.getScaleName() + "测评报告");
report.setReportContent(reportContent);
report.setSummary(summary);
report.setIsGenerated("1");
report.setGenerateTime(DateUtils.getNowDate());
report.setUpdateTime(DateUtils.getNowDate());
reportMapper.updateReport(report);
System.out.println("更新已存在的报告reportId: " + report.getReportId());
}
else
{
// 创建新报告
report = new PsyAssessmentReport();
report.setAssessmentId(assessmentId);
report.setReportType("standard");
report.setReportTitle(scale.getScaleName() + "测评报告");
report.setReportContent(reportContent);
report.setSummary(summary);
report.setIsGenerated("1");
report.setGenerateTime(DateUtils.getNowDate());
report.setCreateBy("system");
report.setCreateTime(DateUtils.getNowDate());
reportMapper.insertReport(report);
System.out.println("创建新报告reportId: " + report.getReportId() + ", assessmentId: " + assessmentId);
}
// 11. 自动预警检测在报告生成后触发
try
@ -205,17 +246,37 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
private BigDecimal calculateFactorScore(PsyFactor factor, List<PsyFactorRule> rules, List<PsyAssessmentAnswer> answers)
{
BigDecimal score = BigDecimal.ZERO;
int matchedCount = 0;
for (PsyFactorRule rule : rules)
{
PsyAssessmentAnswer answer = findAnswerByItemId(answers, rule.getItemId());
if (answer == null)
if (rule.getItemId() == null)
{
System.out.println("警告:因子 " + factor.getFactorName() + " 的计分规则缺少 itemId");
continue;
}
PsyAssessmentAnswer answer = findAnswerByItemId(answers, rule.getItemId());
if (answer == null)
{
System.out.println("警告:因子 " + factor.getFactorName() + " 的计分规则中题目ID " + rule.getItemId() + " 没有找到对应的答案");
continue;
}
if (answer.getAnswerScore() == null)
{
System.out.println("警告题目ID " + rule.getItemId() + " 的答案得分为空");
continue;
}
matchedCount++;
// 根据计分方式计算
String calcType = rule.getCalculationType();
if (calcType == null || calcType.isEmpty())
{
calcType = "sum"; // 默认求和
}
BigDecimal weight = rule.getWeight() != null ? rule.getWeight() : BigDecimal.ONE;
switch (calcType)
@ -224,7 +285,7 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
score = score.add(answer.getAnswerScore().multiply(weight));
break;
case "average":
// 平均分需要特殊处理
// 平均分需要特殊处理先求和最后除以数量
score = score.add(answer.getAnswerScore().multiply(weight));
break;
case "max":
@ -246,6 +307,17 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
}
}
// 如果是平均分计算需要除以匹配的数量
if (matchedCount > 0 && rules.size() > 0)
{
String calcType = rules.get(0).getCalculationType();
if (calcType != null && "average".equals(calcType))
{
score = score.divide(BigDecimal.valueOf(matchedCount), 2, RoundingMode.HALF_UP);
}
}
System.out.println("因子 " + factor.getFactorName() + " (ID: " + factor.getFactorId() + ") 得分: " + score + ", 匹配规则数: " + matchedCount + "/" + rules.size());
return score;
}
@ -263,67 +335,107 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
/**
* 生成报告内容
*/
private String generateReportContent(PsyScale scale, List<PsyFactor> factors, List<PsyFactorScore> factorScores, PsyAssessment assessment)
private String generateReportContent(PsyScale scale, List<PsyFactor> factors, List<PsyFactorScore> factorScores, PsyAssessment assessment, List<PsyAssessmentAnswer> answers)
{
StringBuilder content = new StringBuilder();
content.append("<div class='report-container'>");
content.append("<h1>").append(scale.getScaleName()).append("测评报告</h1>");
content.append("<p class='report-info'>测评时间:").append(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, assessment.getSubmitTime())).append("</p>");
content.append("<p class='report-info'>被测评人:").append(assessment.getAssesseeName()).append("</p>");
content.append("<p class='report-info'>被测评人:").append(assessment.getAssesseeName() != null ? assessment.getAssesseeName() : "未填写").append("</p>");
content.append("<p class='report-info'>总题目数:").append(answers != null ? answers.size() : 0).append("</p>");
content.append("<p class='report-info'>总分:").append(assessment.getTotalScore() != null ? assessment.getTotalScore() : BigDecimal.ZERO).append("</p>");
// 因子得分表格
content.append("<h2>因子得分</h2>");
content.append("<table class='score-table'>");
content.append("<thead><tr><th>因子名称</th><th>得分</th><th>解释</th></tr></thead>");
content.append("<tbody>");
for (PsyFactorScore factorScore : factorScores)
if (factorScores == null || factorScores.isEmpty())
{
// 查找对应的因子
PsyFactor factor = factors.stream()
.filter(f -> f.getFactorId().equals(factorScore.getFactorId()))
.findFirst()
.orElse(null);
if (factor == null)
{
continue;
}
// 查找结果解释
PsyResultInterpretation interpretation = interpretationService.selectInterpretationByScore(
scale.getScaleId(), factor.getFactorId(), factorScore.getFactorScore());
String interpretationText = "";
if (interpretation != null)
{
interpretationText = interpretation.getInterpretationContent() != null
? interpretation.getInterpretationContent() : "";
}
content.append("<tr>");
content.append("<td>").append(factor.getFactorName()).append("</td>");
content.append("<td>").append(factorScore.getFactorScore()).append("</td>");
content.append("<td>").append(interpretationText).append("</td>");
content.append("</tr>");
content.append("<p style='color: #f56c6c;'>警告:未能计算出因子得分。可能原因:</p>");
content.append("<ul>");
content.append("<li>因子没有配置计分规则</li>");
content.append("<li>计分规则中的题目ID与答案中的题目ID不匹配</li>");
content.append("<li>答案得分数据异常</li>");
content.append("</ul>");
content.append("<p>因子数量:").append(factors != null ? factors.size() : 0).append("</p>");
content.append("<p>答案数量:").append(answers != null ? answers.size() : 0).append("</p>");
}
else
{
content.append("<table class='score-table' style='width: 100%; border-collapse: collapse; margin: 20px 0;'>");
content.append("<thead><tr style='background-color: #f5f7fa;'><th style='padding: 10px; border: 1px solid #ddd;'>因子名称</th><th style='padding: 10px; border: 1px solid #ddd;'>得分</th><th style='padding: 10px; border: 1px solid #ddd;'>解释</th></tr></thead>");
content.append("<tbody>");
content.append("</tbody>");
content.append("</table>");
for (PsyFactorScore factorScore : factorScores)
{
// 查找对应的因子
PsyFactor factor = factors.stream()
.filter(f -> f.getFactorId().equals(factorScore.getFactorId()))
.findFirst()
.orElse(null);
if (factor == null)
{
continue;
}
// 查找结果解释
PsyResultInterpretation interpretation = interpretationService.selectInterpretationByScore(
scale.getScaleId(), factor.getFactorId(), factorScore.getFactorScore());
String interpretationText = "";
if (interpretation != null)
{
interpretationText = interpretation.getInterpretationContent() != null
? interpretation.getInterpretationContent() : "";
if (interpretation.getSuggestions() != null && !interpretation.getSuggestions().isEmpty())
{
if (!interpretationText.isEmpty())
{
interpretationText += "<br/>";
}
interpretationText += "<strong>建议:</strong>" + interpretation.getSuggestions();
}
}
else
{
interpretationText = "暂无解释";
}
content.append("<tr>");
content.append("<td style='padding: 10px; border: 1px solid #ddd;'>").append(factor.getFactorName()).append("</td>");
content.append("<td style='padding: 10px; border: 1px solid #ddd; text-align: center;'>").append(factorScore.getFactorScore()).append("</td>");
content.append("<td style='padding: 10px; border: 1px solid #ddd;'>").append(interpretationText).append("</td>");
content.append("</tr>");
}
content.append("</tbody>");
content.append("</table>");
}
// 总体结论
content.append("<h2>总体结论</h2>");
PsyResultInterpretation overallInterpretation = interpretationService.selectInterpretationByScore(
scale.getScaleId(), null, assessment.getTotalScore());
if (overallInterpretation != null)
if (assessment.getTotalScore() != null)
{
content.append("<p>").append(overallInterpretation.getInterpretationContent()).append("</p>");
if (overallInterpretation.getSuggestions() != null)
PsyResultInterpretation overallInterpretation = interpretationService.selectInterpretationByScore(
scale.getScaleId(), null, assessment.getTotalScore());
if (overallInterpretation != null)
{
content.append("<h3>建议</h3>");
content.append("<p>").append(overallInterpretation.getSuggestions()).append("</p>");
content.append("<p>").append(overallInterpretation.getInterpretationContent() != null ? overallInterpretation.getInterpretationContent() : "暂无结论").append("</p>");
if (overallInterpretation.getSuggestions() != null && !overallInterpretation.getSuggestions().isEmpty())
{
content.append("<h3>建议</h3>");
content.append("<p>").append(overallInterpretation.getSuggestions()).append("</p>");
}
}
else
{
content.append("<p>暂无总体结论,请配置总体解释规则。</p>");
}
}
else
{
content.append("<p style='color: #f56c6c;'>警告:总分为空,无法生成总体结论。</p>");
}
content.append("</div>");
@ -333,16 +445,21 @@ public class PsyAssessmentReportServiceImpl implements IPsyAssessmentReportServi
/**
* 生成报告摘要
*/
private String generateSummary(List<PsyFactor> factors, List<PsyFactorScore> factorScores, PsyAssessment assessment)
private String generateSummary(List<PsyFactor> factors, List<PsyFactorScore> factorScores, PsyAssessment assessment, List<PsyAssessmentAnswer> answers)
{
StringBuilder summary = new StringBuilder();
summary.append("本次测评完成于").append(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, assessment.getSubmitTime()));
summary.append(",共评估了").append(factors.size()).append("个因子。");
summary.append("总分:").append(assessment.getTotalScore()).append("");
summary.append(",共评估了").append(factors != null ? factors.size() : 0).append("个因子");
summary.append(",完成").append(answers != null ? answers.size() : 0).append("道题目。");
summary.append("总分:").append(assessment.getTotalScore() != null ? assessment.getTotalScore() : BigDecimal.ZERO).append("");
if (!factorScores.isEmpty())
if (factorScores != null && !factorScores.isEmpty())
{
summary.append("各因子得分情况良好,详见报告正文。");
summary.append("成功计算出").append(factorScores.size()).append("个因子的得分,详见报告正文。");
}
else
{
summary.append("警告:未能计算出因子得分,请检查因子计分规则配置。");
}
return summary.toString();

View File

@ -1,11 +1,36 @@
package com.ddnai.system.service.impl.psychology;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ddnai.system.domain.psychology.PsyScale;
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;
import com.ddnai.system.domain.psychology.vo.ScaleImportVO;
import com.ddnai.system.mapper.psychology.PsyScaleMapper;
import com.ddnai.system.service.psychology.IPsyScaleService;
import com.ddnai.system.service.psychology.IPsyFactorService;
import com.ddnai.system.service.psychology.IPsyScaleItemService;
import com.ddnai.system.service.psychology.IPsyScaleOptionService;
import com.ddnai.system.service.psychology.IPsyFactorRuleService;
import com.ddnai.system.service.psychology.IPsyResultInterpretationService;
import com.ddnai.system.service.psychology.IPsyWarningRuleService;
import com.ddnai.system.service.psychology.IPsyAssessmentService;
import com.ddnai.system.mapper.psychology.PsyAssessmentMapper;
import com.ddnai.system.mapper.psychology.PsyAssessmentAnswerMapper;
import com.ddnai.system.mapper.psychology.PsyAssessmentReportMapper;
import com.ddnai.system.mapper.psychology.PsyFactorScoreMapper;
import com.ddnai.system.mapper.psychology.PsyScalePermissionMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 心理量表 服务层实现
@ -15,8 +40,46 @@ import com.ddnai.system.service.psychology.IPsyScaleService;
@Service
public class PsyScaleServiceImpl implements IPsyScaleService
{
private static final Logger log = LoggerFactory.getLogger(PsyScaleServiceImpl.class);
@Autowired
private PsyScaleMapper scaleMapper;
@Autowired
private IPsyFactorService factorService;
@Autowired
private IPsyScaleItemService itemService;
@Autowired
private IPsyScaleOptionService optionService;
@Autowired
private IPsyFactorRuleService factorRuleService;
@Autowired
private IPsyResultInterpretationService interpretationService;
@Autowired
private IPsyWarningRuleService warningRuleService;
@Autowired
private PsyAssessmentMapper assessmentMapper;
@Autowired
private IPsyAssessmentService assessmentService;
@Autowired
private PsyAssessmentAnswerMapper assessmentAnswerMapper;
@Autowired
private PsyAssessmentReportMapper assessmentReportMapper;
@Autowired
private PsyFactorScoreMapper factorScoreMapper;
@Autowired
private PsyScalePermissionMapper scalePermissionMapper;
/**
* 查询量表信息
@ -73,10 +136,130 @@ public class PsyScaleServiceImpl implements IPsyScaleService
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteScaleByIds(Long[] scaleIds)
{
// 删除每个量表的关联数据
for (Long scaleId : scaleIds)
{
// 1. 查询该量表的所有测评记录
com.ddnai.system.domain.psychology.PsyAssessment assessmentQuery =
new com.ddnai.system.domain.psychology.PsyAssessment();
assessmentQuery.setScaleId(scaleId);
List<com.ddnai.system.domain.psychology.PsyAssessment> assessments =
assessmentService.selectAssessmentList(assessmentQuery);
if (assessments != null && !assessments.isEmpty())
{
Long[] assessmentIds = assessments.stream()
.map(com.ddnai.system.domain.psychology.PsyAssessment::getAssessmentId)
.toArray(Long[]::new);
// 2. 删除测评答案
for (Long assessmentId : assessmentIds)
{
assessmentAnswerMapper.deleteAnswerByAssessmentId(assessmentId);
}
// 3. 删除测评报告
for (Long assessmentId : assessmentIds)
{
com.ddnai.system.domain.psychology.PsyAssessmentReport reportByAssessment =
assessmentReportMapper.selectReportByAssessmentId(assessmentId);
if (reportByAssessment != null)
{
assessmentReportMapper.deleteReportById(reportByAssessment.getReportId());
}
}
// 4. 删除因子得分
for (Long assessmentId : assessmentIds)
{
factorScoreMapper.deleteFactorScoreByAssessmentId(assessmentId);
}
// 5. 删除测评记录
assessmentMapper.deleteAssessmentByScaleIds(new Long[]{scaleId});
}
// 6. 删除量表权限
scalePermissionMapper.deletePermissionByScaleId(scaleId);
}
// 删除量表相关的其他数据因子题目选项规则等
// 注意这些数据的删除应该在Mapper的deleteScaleByIds中通过外键级联删除或者在Service中手动删除
// 由于数据库外键约束我们需要手动删除关联数据
for (Long scaleId : scaleIds)
{
// 删除因子计分规则通过因子删除
List<PsyFactor> factors = factorService.selectFactorListByScaleId(scaleId);
if (factors != null && !factors.isEmpty())
{
for (PsyFactor factor : factors)
{
factorRuleService.deleteRulesByFactorId(factor.getFactorId());
}
Long[] factorIds = factors.stream().map(PsyFactor::getFactorId).toArray(Long[]::new);
factorService.deleteFactorByIds(factorIds);
}
// 删除题目选项通过题目删除
List<PsyScaleItem> items = itemService.selectItemListByScaleId(scaleId);
if (items != null && !items.isEmpty())
{
for (PsyScaleItem item : items)
{
optionService.deleteOptionsByItemId(item.getItemId());
}
Long[] itemIds = items.stream().map(PsyScaleItem::getItemId).toArray(Long[]::new);
itemService.deleteItemByIds(itemIds);
}
// 删除解释配置
PsyResultInterpretation interpretation = new PsyResultInterpretation();
interpretation.setScaleId(scaleId);
List<com.ddnai.system.domain.psychology.PsyResultInterpretation> interpretations =
interpretationService.selectInterpretationList(interpretation);
if (interpretations != null && !interpretations.isEmpty())
{
Long[] interpretationIds = interpretations.stream()
.map(com.ddnai.system.domain.psychology.PsyResultInterpretation::getInterpretationId)
.toArray(Long[]::new);
interpretationService.deleteInterpretationByIds(interpretationIds);
}
// 删除预警规则
com.ddnai.system.domain.psychology.PsyWarningRule warningRule =
new com.ddnai.system.domain.psychology.PsyWarningRule();
warningRule.setScaleId(scaleId);
List<com.ddnai.system.domain.psychology.PsyWarningRule> warningRules =
warningRuleService.selectWarningRuleList(warningRule);
if (warningRules != null && !warningRules.isEmpty())
{
Long[] ruleIds = warningRules.stream()
.map(com.ddnai.system.domain.psychology.PsyWarningRule::getRuleId)
.toArray(Long[]::new);
warningRuleService.deleteWarningRuleByIds(ruleIds);
}
}
// 最后删除量表
return scaleMapper.deleteScaleByIds(scaleIds);
}
/**
* 检查量表是否被使用
*
* @param scaleId 量表ID
* @return 是否被使用
*/
@Override
public boolean isScaleInUse(Long scaleId)
{
int count = scaleMapper.countAssessmentByScaleId(scaleId);
return count > 0;
}
/**
* 检查量表编码是否唯一
@ -90,5 +273,370 @@ public class PsyScaleServiceImpl implements IPsyScaleService
int count = scaleMapper.checkScaleCodeUnique(scaleCode);
return count == 0;
}
/**
* 导入量表原有方法保持向后兼容
*
* @param importData 导入数据
* @param username 操作人
* @return 新插入的量表ID
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long importScale(ScaleImportVO importData, String username)
{
// 调用支持factorCode映射的版本传入null作为rawJsonData
return importScale(importData, null, username);
}
/**
* 导入量表支持factorCode映射
*
* @param importData 导入数据
* @param rawJsonData 原始JSON数据用于提取factorCode可为null
* @param username 操作人
* @return 新插入的量表ID
*/
@Transactional(rollbackFor = Exception.class)
public Long importScale(ScaleImportVO importData, java.util.Map<String, Object> rawJsonData, String username)
{
if (importData == null || importData.getScale() == null)
{
throw new RuntimeException("导入数据不能为空");
}
PsyScale scale = importData.getScale();
// 检查量表编码是否已存在
if (!checkScaleCodeUnique(scale.getScaleCode()))
{
throw new RuntimeException("量表编码已存在:" + scale.getScaleCode());
}
// 设置创建人
scale.setCreateBy(username);
scale.setItemCount(null); // 题目数量由系统自动计算不导入
// 1. 插入量表基本信息
int insertResult = scaleMapper.insertScale(scale);
Long scaleId = scale.getScaleId();
// 验证scaleId是否成功获取
if (scaleId == null)
{
log.error("插入量表失败无法获取量表ID。insertResult: {}", insertResult);
throw new RuntimeException("插入量表失败无法获取量表ID。请检查数据库配置和主键生成策略。");
}
log.info("插入量表成功scaleId: {}, 量表名称: {}", scaleId, scale.getScaleName());
// 2. 建立因子映射旧因子编码 -> 新因子ID
Map<String, Long> factorCodeMap = new HashMap<>();
if (importData.getFactors() != null && !importData.getFactors().isEmpty())
{
log.info("开始导入因子,共 {} 个", importData.getFactors().size());
for (ScaleImportVO.FactorImportVO factorVO : importData.getFactors())
{
PsyFactor factor = factorVO.getFactor();
if (factor != null)
{
try
{
// 确保scaleId已设置
if (factor.getScaleId() == null)
{
factor.setScaleId(scaleId);
}
// 确保创建人已设置
if (StringUtils.isBlank(factor.getCreateBy()))
{
factor.setCreateBy(username);
}
log.debug("插入因子: {} - {}", factor.getFactorCode(), factor.getFactorName());
factorService.insertFactor(factor);
// 验证因子ID是否成功获取
if (factor.getFactorId() == null)
{
log.error("插入因子失败无法获取因子ID。因子编码: {}", factor.getFactorCode());
throw new RuntimeException("插入因子失败无法获取因子ID " + factor.getFactorCode());
}
// 保存因子编码映射
if (StringUtils.isNotBlank(factor.getFactorCode()))
{
factorCodeMap.put(factor.getFactorCode(), factor.getFactorId());
log.debug("因子映射: {} -> {}", factor.getFactorCode(), factor.getFactorId());
}
}
catch (Exception e)
{
log.error("插入因子失败: {} - {}, 错误: {}",
factor.getFactorCode(), factor.getFactorName(), e.getMessage(), e);
throw new RuntimeException("插入因子失败: " + factor.getFactorName() + ", " + e.getMessage(), e);
}
}
}
log.info("因子导入完成,共导入 {} 个因子", factorCodeMap.size());
}
// 3. 建立题目映射旧题目序号 -> 新题目ID
Map<Integer, Long> itemNumberMap = new HashMap<>();
if (importData.getItems() != null && !importData.getItems().isEmpty())
{
log.info("开始导入题目,共 {} 个", importData.getItems().size());
int successCount = 0;
for (ScaleImportVO.ItemImportVO itemVO : importData.getItems())
{
PsyScaleItem item = itemVO.getItem();
if (item != null)
{
try
{
// 确保scaleId已设置
if (item.getScaleId() == null)
{
item.setScaleId(scaleId);
}
// 确保创建人已设置
if (StringUtils.isBlank(item.getCreateBy()))
{
item.setCreateBy(username);
}
log.debug("插入题目: {} - {}", item.getItemNumber(),
item.getItemContent() != null && item.getItemContent().length() > 20
? item.getItemContent().substring(0, 20) + "..."
: item.getItemContent());
itemService.insertItem(item);
Long itemId = item.getItemId();
Integer itemNumber = item.getItemNumber();
// 验证题目ID是否成功获取
if (itemId == null)
{
log.error("插入题目失败无法获取题目ID。题目序号: {}", itemNumber);
throw new RuntimeException("插入题目失败无法获取题目ID题目 " + itemNumber);
}
// 保存题目序号映射
if (itemNumber != null)
{
itemNumberMap.put(itemNumber, itemId);
}
// 插入题目选项
if (itemVO.getOptions() != null && !itemVO.getOptions().isEmpty())
{
for (PsyScaleOption option : itemVO.getOptions())
{
option.setItemId(itemId);
if (StringUtils.isBlank(option.getCreateBy()))
{
option.setCreateBy(username);
}
}
optionService.saveOptions(itemId, itemVO.getOptions());
log.debug("题目 {} 的 {} 个选项已保存", itemNumber, itemVO.getOptions().size());
}
successCount++;
}
catch (Exception e)
{
log.error("插入题目失败: 题目序号 {}, 错误: {}",
item.getItemNumber(), e.getMessage(), e);
throw new RuntimeException("插入题目失败: 题目 " + item.getItemNumber() + ", " + e.getMessage(), e);
}
}
}
log.info("题目导入完成,成功导入 {} 个题目", successCount);
}
// 4. 插入计分规则需要映射因子和题目ID
if (importData.getFactors() != null && !importData.getFactors().isEmpty())
{
log.info("开始导入计分规则");
int ruleCount = 0;
for (ScaleImportVO.FactorImportVO factorVO : importData.getFactors())
{
if (factorVO.getRules() != null && !factorVO.getRules().isEmpty())
{
Long factorId = factorCodeMap.get(factorVO.getFactor().getFactorCode());
if (factorId != null)
{
List<PsyFactorRule> rules = new java.util.ArrayList<>();
for (ScaleImportVO.FactorRuleImportVO ruleVO : factorVO.getRules())
{
PsyFactorRule rule = ruleVO.getRule();
if (rule != null)
{
// 通过题目序号映射到新的题目ID
Integer itemNumber = ruleVO.getItemNumber();
if (itemNumber != null)
{
Long newItemId = itemNumberMap.get(itemNumber);
if (newItemId != null)
{
rule.setItemId(newItemId);
}
else
{
log.warn("找不到题目序号 {} 对应的题目ID跳过该计分规则", itemNumber);
continue;
}
}
rule.setFactorId(factorId);
if (StringUtils.isBlank(rule.getCreateBy()))
{
rule.setCreateBy(username);
}
rules.add(rule);
ruleCount++;
}
}
if (!rules.isEmpty())
{
factorRuleService.saveRules(factorId, rules);
log.debug("因子 {} 的 {} 条计分规则已保存", factorVO.getFactor().getFactorCode(), rules.size());
}
}
else
{
log.warn("找不到因子编码 {} 对应的因子ID跳过计分规则", factorVO.getFactor().getFactorCode());
}
}
}
log.info("计分规则导入完成,共导入 {} 条规则", ruleCount);
}
// 5. 插入解释配置需要映射因子ID
if (importData.getInterpretations() != null && !importData.getInterpretations().isEmpty())
{
log.info("开始导入解释配置,共 {} 条", importData.getInterpretations().size());
int interpretationCount = 0;
// 从原始JSON中提取factorCode信息并映射到factorId
for (int i = 0; i < importData.getInterpretations().size(); i++)
{
PsyResultInterpretation interpretation = importData.getInterpretations().get(i);
interpretation.setScaleId(scaleId);
// 如果factorId为null尝试从原始JSON中获取factorCode并映射
if (interpretation.getFactorId() == null && rawJsonData != null && rawJsonData.containsKey("interpretations"))
{
Object interpretationsObj = rawJsonData.get("interpretations");
if (interpretationsObj instanceof java.util.List)
{
java.util.List<?> rawInterpretations = (java.util.List<?>) interpretationsObj;
if (i < rawInterpretations.size())
{
Object rawInterpretationObj = rawInterpretations.get(i);
if (rawInterpretationObj instanceof java.util.Map)
{
java.util.Map<?, ?> rawMap = (java.util.Map<?, ?>) rawInterpretationObj;
if (rawMap.containsKey("factorCode") && rawMap.get("factorCode") != null)
{
String factorCode = rawMap.get("factorCode").toString();
Long mappedFactorId = factorCodeMap.get(factorCode);
if (mappedFactorId != null)
{
interpretation.setFactorId(mappedFactorId);
log.debug("通过factorCode {} 映射到factorId {}", factorCode, mappedFactorId);
}
else
{
log.warn("找不到factorCode {} 对应的factorId将作为总体解释", factorCode);
}
}
}
}
}
}
if (StringUtils.isBlank(interpretation.getCreateBy()))
{
interpretation.setCreateBy(username);
}
try
{
interpretationService.insertInterpretation(interpretation);
interpretationCount++;
}
catch (Exception e)
{
log.error("插入解释配置失败: {}, 错误: {}", interpretation.getInterpretationTitle(), e.getMessage());
}
}
log.info("解释配置导入完成,成功导入 {} 条", interpretationCount);
}
// 6. 插入预警规则需要映射因子ID
if (importData.getWarningRules() != null && !importData.getWarningRules().isEmpty())
{
log.info("开始导入预警规则,共 {} 条", importData.getWarningRules().size());
int warningRuleCount = 0;
// 从原始JSON中提取factorCode信息
for (int i = 0; i < importData.getWarningRules().size(); i++)
{
PsyWarningRule warningRule = importData.getWarningRules().get(i);
warningRule.setScaleId(scaleId);
// 如果factorId为null尝试从原始JSON中获取factorCode并映射
if (warningRule.getFactorId() == null && rawJsonData != null && rawJsonData.containsKey("warningRules"))
{
Object warningRulesObj = rawJsonData.get("warningRules");
if (warningRulesObj instanceof java.util.List)
{
java.util.List<?> rawWarningRules = (java.util.List<?>) warningRulesObj;
if (i < rawWarningRules.size())
{
Object rawWarningRuleObj = rawWarningRules.get(i);
if (rawWarningRuleObj instanceof java.util.Map)
{
java.util.Map<?, ?> rawMap = (java.util.Map<?, ?>) rawWarningRuleObj;
if (rawMap.containsKey("factorCode") && rawMap.get("factorCode") != null)
{
String factorCode = rawMap.get("factorCode").toString();
Long mappedFactorId = factorCodeMap.get(factorCode);
if (mappedFactorId != null)
{
warningRule.setFactorId(mappedFactorId);
log.debug("通过factorCode {} 映射到factorId {}", factorCode, mappedFactorId);
}
else
{
log.warn("找不到factorCode {} 对应的factorId将作为总体预警规则", factorCode);
}
}
}
}
}
}
if (StringUtils.isBlank(warningRule.getCreateBy()))
{
warningRule.setCreateBy(username);
}
try
{
warningRuleService.insertWarningRule(warningRule);
warningRuleCount++;
}
catch (Exception e)
{
log.error("插入预警规则失败: {}, 错误: {}", warningRule.getRuleName(), e.getMessage());
}
}
log.info("预警规则导入完成,成功导入 {} 条", warningRuleCount);
}
log.info("量表导入完成scaleId: {}", scaleId);
return scaleId;
}
}

View File

@ -0,0 +1,40 @@
package com.ddnai.system.service.psychology;
import org.springframework.web.multipart.MultipartFile;
import com.ddnai.system.domain.psychology.vo.ScaleImportVO;
/**
* 文档解析服务接口
* 用于解析PDFDOCX等格式的量表文档
*
* @author ddnai
*/
public interface IDocumentParseService
{
/**
* 解析文档并转换为量表导入数据
*
* @param file 上传的文件PDF或DOCX
* @return 量表导入数据
* @throws Exception 解析异常
*/
ScaleImportVO parseDocument(MultipartFile file) throws Exception;
/**
* 判断文件类型是否支持
*
* @param filename 文件名
* @return 是否支持
*/
boolean isSupported(String filename);
/**
* 提取文档文本内容
*
* @param file 上传的文件
* @return 文档文本内容
* @throws Exception 解析异常
*/
String extractText(MultipartFile file) throws Exception;
}

View File

@ -57,5 +57,33 @@ public interface IPsyScaleService
* @return 结果
*/
public boolean checkScaleCodeUnique(String scaleCode);
/**
* 导入量表
*
* @param importData 导入数据
* @param username 操作人
* @return 结果
*/
public Long importScale(com.ddnai.system.domain.psychology.vo.ScaleImportVO importData, String username);
/**
* 导入量表支持factorCode映射
*
* @param importData 导入数据
* @param rawJsonData 原始JSON数据用于提取factorCode
* @param username 操作人
* @return 结果
*/
public Long importScale(com.ddnai.system.domain.psychology.vo.ScaleImportVO importData,
java.util.Map<String, Object> rawJsonData, String username);
/**
* 检查量表是否被使用
*
* @param scaleId 量表ID
* @return 是否被使用
*/
public boolean isScaleInUse(Long scaleId);
}

View File

@ -163,6 +163,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{assessmentId}
</foreach>
</delete>
<select id="countAssessmentByScaleId" parameterType="Long" resultType="int">
select count(1) from psy_assessment where scale_id = #{scaleId}
</select>
<delete id="deleteAssessmentByScaleIds" parameterType="String">
delete from psy_assessment where scale_id in
<foreach item="scaleId" collection="array" open="(" separator="," close=")">
#{scaleId}
</foreach>
</delete>
</mapper>

View File

@ -52,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertFactor" parameterType="com.ddnai.system.domain.psychology.PsyFactor" useGeneratedKeys="true" keyProperty="factorId">
insert into psy_factor (
<if test="scaleId != null">scale_id, </if>
scale_id,
<if test="factorCode != null and factorCode != ''">factor_code, </if>
<if test="factorName != null and factorName != ''">factor_name, </if>
<if test="factorEnName != null">factor_en_name, </if>
@ -62,7 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="scaleId != null">#{scaleId}, </if>
#{scaleId},
<if test="factorCode != null and factorCode != ''">#{factorCode}, </if>
<if test="factorName != null and factorName != ''">#{factorName}, </if>
<if test="factorEnName != null">#{factorEnName}, </if>

View File

@ -71,7 +71,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by s.sort_order, s.create_time desc
</select>
<insert id="insertScale" parameterType="com.ddnai.system.domain.psychology.PsyScale">
<insert id="insertScale" parameterType="com.ddnai.system.domain.psychology.PsyScale" useGeneratedKeys="true" keyProperty="scaleId">
insert into psy_scale (
<if test="scaleCode != null and scaleCode != ''">scale_code, </if>
<if test="scaleName != null and scaleName != ''">scale_name, </if>
@ -152,6 +152,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="checkScaleCodeUnique" parameterType="String" resultType="int">
select count(1) from psy_scale where scale_code = #{scaleCode}
</select>
<select id="countAssessmentByScaleId" parameterType="Long" resultType="int">
select count(1) from psy_assessment where scale_id = #{scaleId}
</select>
</mapper>

View File

@ -0,0 +1,152 @@
# 管理员快速填充功能说明
## 📋 功能概述
管理员在进行量表测评时,可以使用"快速填充"功能,自动选择所有题目的特定选项并提交测评。此功能主要用于测试和快速验证量表功能。
## 🎯 功能特性
### 1. 管理员专用
- 只有管理员userId === 1 或 roles 包含 'admin')才能看到和使用此功能
- 普通用户无法看到快速填充按钮
### 2. 快速填充策略
系统提供4种填充策略
#### ① 填充第一个选项并提交
- 为所有题目选择第一个选项选项A或第一个选项
- 适用于测试最低分或基础功能
#### ② 填充中间选项并提交
- 为所有题目选择中间选项
- 例如5个选项中选择第3个4个选项中选择第2个
#### ③ 填充最后一个选项并提交
- 为所有题目选择最后一个选项
- 适用于测试最高分或极端情况
#### ④ 随机填充并提交
- 为每个题目随机选择一个选项
- 适用于模拟真实答题情况
### 3. 自动处理
- 自动处理单选题、多选题、矩阵题
- 自动计算得分
- 自动保存所有答案
- 自动提交测评并生成报告
## 📝 使用方法
### 步骤1进入测评页面
1. 登录系统(管理员账户)
2. 进入 `心理测评管理``测评记录`
3. 点击 **"开始测评"** 或 **"继续测评"**
### 步骤2使用快速填充
1. 在测评页面顶部,找到 **"快速填充"** 按钮
2. 点击下拉箭头,选择填充策略:
- 填充第一个选项并提交
- 填充中间选项并提交
- 填充最后一个选项并提交
- 随机填充并提交
### 步骤3确认并执行
1. 系统会弹出确认对话框
2. 点击 **"确定"** 后,系统会:
- 自动填充所有题目
- 保存所有答案
- 提交测评
- 生成测评报告
3. 完成后自动跳转到测评记录页面
## ⚙️ 技术实现
### 前端实现
- **文件位置**`ruoyi-ui/src/views/psychology/assessment/taking.vue`
- **关键方法**
- `handleQuickFill(command)`: 处理快速填充命令
- `performQuickFill(command)`: 执行快速填充逻辑
- `saveAnswerToServerPromise(answer)`: 保存答案到服务器
### 权限控制
- 使用 `isAdmin` 计算属性判断管理员身份
- 通过 `v-if="isAdmin"` 控制按钮显示
- 在方法中再次验证权限
### 填充逻辑
```javascript
// 选项选择策略
switch (command) {
case 'first': // 第一个选项
case 'middle': // 中间选项
case 'last': // 最后一个选项
case 'random': // 随机选项
}
// 自动保存和提交
Promise.all(fillPromises).then(() => {
submitAssessment(this.assessmentId)
})
```
## 🔒 安全特性
1. **权限验证**:双重验证管理员身份
2. **确认对话框**:防止误操作
3. **错误处理**:完善的错误处理和提示
4. **日志记录**:记录填充操作的日志
## ⚠️ 注意事项
1. **不可撤销**:快速填充并提交后,测评结果无法修改
2. **覆盖已有答案**:如果题目已有答案,快速填充会覆盖原有答案
3. **多选题处理**:多选题目前只选择第一个选项(可根据需要调整)
4. **测试用途**:此功能主要用于测试,不建议用于真实测评数据
## 📊 使用场景
### 适用场景
- ✅ 测试量表功能是否正常
- ✅ 快速生成测试报告
- ✅ 验证报告生成功能
- ✅ 系统功能演示
### 不适用场景
- ❌ 真实用户测评
- ❌ 正式的数据收集
- ❌ 需要准确答案的测评
## 🔧 自定义配置
如果需要修改填充策略,可以编辑 `performQuickFill` 方法中的 `optionStrategy` 函数。
例如,修改多选题策略为选择所有选项:
```javascript
else if (item.itemType === 'multiple') {
// 多选题,选择所有选项
selectedOptions = options.map(opt => opt.optionId);
const totalScore = options.reduce((sum, opt) => sum + (opt.optionScore || 0), 0);
// ...
}
```
## ✅ 验证清单
使用前请确认:
- [ ] 当前用户是管理员
- [ ] 测评状态为"进行中"
- [ ] 量表包含题目
- [ ] 所有题目都有选项
## 📚 相关文件
- `ruoyi-ui/src/views/psychology/assessment/taking.vue` - 测评页面
- `ruoyi-ui/src/api/psychology/assessment.js` - 测评API
## 🎉 更新日志
- **2025-01-XX**:添加管理员快速填充功能
- 支持4种填充策略
- 自动保存和提交
- 完善的权限控制和错误处理

View File

@ -0,0 +1,692 @@
# 量表导入JSON标准格式说明
## 📋 概述
本文档详细说明如何使用JSON格式导入量表数据到系统中。JSON格式适用于批量导入完整的量表数据包括量表基本信息、题目、选项、因子、计分规则等。
## 🎯 导入方式
系统支持三种导入方式:
1. **JSON文本导入**在导入界面直接粘贴JSON文本
2. **JSON文件导入**:上传.json格式的文件
3. **PDF/DOCX文档导入**系统自动解析文档并转换为JSON格式自动识别
---
## 📝 JSON数据结构
### 完整JSON结构
```json
{
"scale": {
// 量表基本信息
},
"items": [
// 题目列表(包含选项)
],
"factors": [
// 因子列表(包含计分规则)
],
"interpretations": [
// 结果解释配置(可选)
],
"warningRules": [
// 预警规则配置(可选)
]
}
```
---
## 🔍 详细字段说明
### 1. 量表基本信息 (scale)
```json
{
"scaleCode": "SCL_90", // 量表编码(必填,唯一)
"scaleName": "症状自评量表SCL-90", // 量表名称(必填)
"scaleEnName": "Symptom Checklist 90", // 量表英文名称(可选)
"scaleType": "symptom", // 量表类型(可选)
"scaleVersion": "1.0", // 量表版本(可选)
"scaleIntro": "SCL-90量表简介", // 量表简介(可选)
"scaleDescription": "SCL-90详细描述", // 量表描述(可选)
"itemCount": 90, // 题目数量(可选,系统自动计算)
"estimatedTime": 30, // 预计完成时间(分钟)(可选)
"targetPopulation": "一般人群", // 适用人群(可选)
"author": "Derogatis", // 作者(可选)
"source": "心理卫生评定量表手册", // 来源(可选)
"reference": "参考文献", // 参考文献(可选)
"status": "0", // 状态0-正常1-停用(必填,默认"0"
"sortOrder": 0 // 排序顺序可选默认0
}
```
**字段说明**
- `scaleCode`:必须唯一,建议使用英文大写字母和下划线,如 `SCL_90`、`EPQ_001`
- `scaleName`:量表的完整中文名称
- `scaleType`:量表类型,如 `symptom`(症状)、`personality`(人格)、`emotion`(情绪)等
- `status``"0"` 表示正常(启用),`"1"` 表示停用
---
### 2. 题目列表 (items)
```json
{
"items": [
{
"item": {
"itemNumber": 1, // 题目序号(必填)
"itemContent": "头痛", // 题目内容(必填)
"itemType": "single", // 题目类型single-单选multiple-多选(必填)
"required": "1", // 是否必答0-否1-是(必填,默认"1"
"reverseScore": "0", // 是否反向计分0-否1-是(必填,默认"0"
"sortOrder": 1 // 排序顺序(可选)
},
"options": [
// 选项列表
]
}
]
}
```
**字段说明**
- `itemNumber`题目序号从1开始建议连续
- `itemContent`:题目的完整文字内容
- `itemType``"single"` 表示单选题,`"multiple"` 表示多选题
- `required``"1"` 表示必答,`"0"` 表示可选
- `reverseScore``"1"` 表示反向计分(得分越高表示程度越低),`"0"` 表示正常计分
---
### 3. 选项列表 (options)
每个题目包含一个选项数组:
```json
{
"options": [
{
"optionCode": "A", // 选项编码(必填)
"optionContent": "没有", // 选项内容(必填)
"optionScore": 0, // 选项分数(必填)
"sortOrder": 1 // 排序顺序(可选)
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
}
```
**字段说明**
- `optionCode`:选项编码,通常使用 A、B、C、D、E 或 1、2、3、4、5
- `optionContent`:选项的文字描述
- `optionScore`:该选项对应的分数(数字类型)
- `sortOrder`:选项的显示顺序,数字越小越靠前
---
### 4. 因子列表 (factors)
```json
{
"factors": [
{
"factor": {
"factorCode": "F1", // 因子编码(必填)
"factorName": "躯体化", // 因子名称(必填)
"factorEnName": "Somatization", // 因子英文名称(可选)
"factorDescription": "躯体化因子描述", // 因子描述(可选)
"factorOrder": 1 // 因子排序(可选)
},
"rules": [
// 计分规则列表
]
}
]
}
```
**字段说明**
- `factorCode`:因子编码,建议使用 F1、F2、F3 等格式
- `factorName`:因子的中文名称
- `factorOrder`:因子的排序顺序
---
### 5. 计分规则 (rules)
每个因子包含一个计分规则数组:
```json
{
"rules": [
{
"itemNumber": 1, // 题目序号(必填,用于映射)
"rule": {
"optionIds": "1,2,3,4,5", // 选项ID列表逗号分隔可选留空表示所有选项
"weight": 1.0, // 权重必填默认1.0
"calculationType": "sum" // 计算方式sum-求和avg-平均max-最大min-最小(必填)
}
},
{
"itemNumber": 4,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
}
```
**字段说明**
- `itemNumber`:题目序号,用于将规则映射到对应的题目
- `optionIds`选项ID列表逗号分隔。如果为空字符串表示使用该题目的所有选项
- `weight`权重用于加权计算默认1.0
- `calculationType`
- `"sum"`:求和(所有选中选项的分数之和 × 权重)
- `"avg"`:平均(所有选中选项的分数平均值 × 权重)
- `"max"`:最大(所有选中选项的分数最大值 × 权重)
- `"min"`:最小(所有选中选项的分数最小值 × 权重)
**注意**`itemNumber` 必须与 `items` 数组中的题目序号对应。
---
### 6. 结果解释 (interpretations) - 可选
```json
{
"interpretations": [
{
"scaleId": null, // 量表ID导入时自动设置无需填写
"factorId": null, // 因子ID导入时自动设置可选
"scoreMin": 0, // 分数下限(必填)
"scoreMax": 10, // 分数上限(必填)
"level": "低", // 等级(可选)
"levelName": "正常范围", // 等级名称(可选)
"interpretationTitle": "正常范围", // 解释标题(必填)
"interpretationContent": "您的得分在正常范围内", // 解释内容(可选)
"suggestions": "继续保持良好的心理状态", // 建议指导(可选)
"sortOrder": 1 // 排序顺序(可选)
}
]
}
```
**字段说明**
- `scoreMin``scoreMax`:定义分数范围,系统会根据实际得分匹配对应的解释
- `factorId`如果留空null表示针对量表总体如果填写表示针对特定因子
---
### 7. 预警规则 (warningRules) - 可选
```json
{
"warningRules": [
{
"scaleId": null, // 量表ID导入时自动设置无需填写
"factorId": null, // 因子ID导入时自动设置可选
"ruleName": "重度抑郁预警", // 规则名称(必填)
"warningLevel": "高", // 预警等级:低、中、高、紧急(必填)
"scoreMin": 30, // 分数下限(可选)
"scoreMax": 40, // 分数上限(可选)
"percentileMin": 90, // 百分位下限(可选)
"percentileMax": 100, // 百分位上限(可选)
"autoResolve": "0", // 是否自动解除0-否1-是(可选)
"resolveCondition": "", // 解除条件(可选)
"status": "0" // 状态0-正常1-停用(必填)
}
]
}
```
---
## 📄 完整JSON示例
### 示例1简单的5题量表无因子
```json
{
"scale": {
"scaleCode": "TEST_001",
"scaleName": "测试量表",
"scaleType": "general",
"status": "0",
"itemCount": 5
},
"items": [
{
"item": {
"itemNumber": 1,
"itemContent": "您是否经常感到焦虑?",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 1
},
"options": [
{
"optionCode": "A",
"optionContent": "从不",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "偶尔",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "经常",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "总是",
"optionScore": 3,
"sortOrder": 4
}
]
},
{
"item": {
"itemNumber": 2,
"itemContent": "您是否容易紧张?",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 2
},
"options": [
{
"optionCode": "A",
"optionContent": "从不",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "偶尔",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "经常",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "总是",
"optionScore": 3,
"sortOrder": 4
}
]
}
],
"factors": [],
"interpretations": [
{
"scoreMin": 0,
"scoreMax": 5,
"level": "低",
"levelName": "正常",
"interpretationTitle": "正常范围",
"interpretationContent": "您的焦虑水平在正常范围内"
},
{
"scoreMin": 6,
"scoreMax": 10,
"level": "中",
"levelName": "轻度",
"interpretationTitle": "轻度焦虑",
"interpretationContent": "您可能存在轻度焦虑,建议适当放松"
},
{
"scoreMin": 11,
"scoreMax": 15,
"level": "高",
"levelName": "中度",
"interpretationTitle": "中度焦虑",
"interpretationContent": "您可能存在中度焦虑,建议寻求专业帮助"
}
],
"warningRules": []
}
```
### 示例2SCL-90量表含因子和计分规则
```json
{
"scale": {
"scaleCode": "SCL_90",
"scaleName": "症状自评量表SCL-90",
"scaleEnName": "Symptom Checklist 90",
"scaleType": "symptom",
"scaleVersion": "1.0",
"scaleIntro": "SCL-90是一个包含90个项目的症状自评量表",
"scaleDescription": "详细描述...",
"itemCount": 90,
"estimatedTime": 30,
"targetPopulation": "一般人群",
"author": "Derogatis",
"source": "心理卫生评定量表手册",
"status": "0"
},
"items": [
{
"item": {
"itemNumber": 1,
"itemContent": "头痛",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 1
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
},
{
"item": {
"itemNumber": 2,
"itemContent": "神经过敏,心中不踏实",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 2
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
}
],
"factors": [
{
"factor": {
"factorCode": "F1",
"factorName": "躯体化",
"factorDescription": "躯体化因子",
"factorOrder": 1
},
"rules": [
{
"itemNumber": 1,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
},
{
"itemNumber": 4,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
},
{
"factor": {
"factorCode": "F5",
"factorName": "焦虑",
"factorDescription": "焦虑因子",
"factorOrder": 5
},
"rules": [
{
"itemNumber": 2,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
}
],
"interpretations": [
{
"factorId": null,
"scoreMin": 0,
"scoreMax": 160,
"level": "低",
"levelName": "正常",
"interpretationTitle": "正常范围",
"interpretationContent": "您的总体得分在正常范围内"
},
{
"factorId": null,
"scoreMin": 161,
"scoreMax": 250,
"level": "中",
"levelName": "轻度",
"interpretationTitle": "轻度症状",
"interpretationContent": "您可能存在轻度心理症状"
}
],
"warningRules": [
{
"factorId": null,
"ruleName": "重度症状预警",
"warningLevel": "高",
"scoreMin": 250,
"scoreMax": 360,
"status": "0"
}
]
}
```
---
## 🚀 使用步骤
### 方式1JSON文本导入
1. 进入 `心理测评管理``量表管理`
2. 点击 **"导入"** 按钮
3. 在弹出对话框中,切换到 **"JSON文本"** 标签页
4. 将JSON文本粘贴到文本框中
5. 点击 **"确定"** 按钮导入
### 方式2JSON文件导入
1. 将JSON数据保存为 `.json` 文件
2. 进入 `心理测评管理``量表管理`
3. 点击 **"导入"** 按钮
4. 在弹出对话框中,切换到 **"文件上传"** 标签页
5. 选择JSON文件并上传
6. 点击 **"确定"** 按钮导入
### 方式3PDF/DOCX文档导入自动解析
1. 准备PDF或DOCX格式的量表文档
2. 进入 `心理测评管理``量表管理`
3. 点击 **"导入"** 按钮
4. 在弹出对话框中,切换到 **"文件上传"** 标签页
5. 选择PDF/DOCX文件并上传
6. 点击 **"预览解析结果"** 查看系统自动识别的结果
7. 根据需要调整JSON数据
8. 点击 **"确定"** 按钮导入
---
## ⚠️ 注意事项
### 1. 数据完整性
- `scale` 对象必须包含 `scaleCode``scaleName`
- 每个题目必须至少包含1个选项
- 如果配置了因子必须为每个因子配置至少1条计分规则
### 2. 数据唯一性
- `scaleCode` 必须在系统中唯一
- 同一量表内的 `factorCode` 必须唯一
- 同一题目内的 `optionCode` 必须唯一
### 3. 数据映射
- 计分规则中的 `itemNumber` 必须与 `items` 数组中的题目序号对应
- 如果配置了 `interpretations``warningRules`,其中的 `factorId` 会在导入时自动映射
### 4. 字段类型
- 数字字段(`itemNumber`、`optionScore`、`weight`等)使用数字类型,不要加引号
- 字符串字段(`scaleCode`、`scaleName`等)使用双引号
- 布尔值字段(`status`、`required`等)使用字符串 `"0"``"1"`
### 5. 可选字段
- `interpretations``warningRules` 是可选的,可以省略或设置为空数组 `[]`
- 如果量表没有因子,`factors` 可以设置为空数组 `[]`
- 大部分字段都有默认值,可以省略
---
## 🔍 常见错误
### 错误1JSON格式错误
**错误信息**`JSON格式错误...`
**解决方法**
- 检查JSON语法确保所有括号、引号匹配
- 使用JSON验证工具如 jsonlint.com验证JSON格式
- 确保没有多余的逗号
### 错误2量表编码已存在
**错误信息**`量表编码已存在XXX`
**解决方法**
- 修改 `scaleCode` 为唯一值
- 或先删除系统中已存在的量表
### 错误3题目序号映射失败
**错误信息**`找不到题目序号 X 对应的题目ID`
**解决方法**
- 检查 `factors[].rules[].itemNumber` 是否与 `items[].item.itemNumber` 对应
- 确保题目序号从1开始连续编号
### 错误4必填字段缺失
**错误信息**`XXX不能为空`
**解决方法**
- 检查必填字段是否都已填写
- 参考本文档的字段说明,补充缺失的字段
---
## 📚 相关文档
- [新量表导入完整操作指南](./15-新量表导入完整操作指南.md) - 手动操作步骤
- [ScaleImportVO.java](../../ry-news-system/src/main/java/com/ddnai/system/domain/psychology/vo/ScaleImportVO.java) - 数据结构定义
---
## ✅ 验证清单
导入前请检查:
- [ ] JSON格式正确可以使用JSON验证工具验证
- [ ] `scaleCode` 唯一且符合命名规范
- [ ] `scaleName` 已填写
- [ ] 所有题目都有 `itemNumber``itemContent`
- [ ] 所有题目都至少包含1个选项
- [ ] 所有选项都有 `optionCode`、`optionContent` 和 `optionScore`
- [ ] 如果有因子,所有因子都有 `factorCode``factorName`
- [ ] 如果有因子,所有计分规则的 `itemNumber` 都能在题目列表中找到
- [ ] `status` 字段设置为 `"0"`(正常)或 `"1"`(停用)
---
**最后更新**2025-01-XX

View File

@ -0,0 +1,73 @@
# 量表示例文件说明
本文件夹包含用于测试量表导入功能的示例文件。
## 文件说明
1. **scale-import-example.json** - JSON格式导入标准示例SCL-90量表示例包含5个题目和因子配置
2. **SCL90症状自评量表含常模.pdf** - SCL-90量表PDF文档可用于PDF导入测试
3. **下载说明.md** - 量表下载来源说明
## 使用说明
### JSON格式导入推荐
1. **使用JSON示例文件**
- 打开 `scale-import-example.json` 查看标准JSON格式
- 参考格式编写您自己的量表JSON数据
- 在系统"量表导入"功能中选择"JSON文本"或"JSON文件"导入
2. **JSON格式说明**
- 详细格式说明请参考:`../16-量表导入JSON标准格式说明.md`
- 该文档包含完整的字段说明、示例和注意事项
### PDF/DOCX文档导入
1. **获取完整的量表文档**PDF或DOCX格式
- 可以从心理评估相关网站下载
- 或者从心理测量工具包中获取
2. **推荐的量表下载来源**
- 优路教育https://www.youlu.com/ziliao/detail/pack-52513
- 心理卫生评定量表手册
- 常用心理评估量表手册
3. **导入测试步骤**
- 将PDF或DOCX文件上传到系统的"量表导入"功能
- 使用"预览解析结果"查看识别效果
- 根据识别结果调整数据可以切换到JSON文本模式手动调整
- 确认无误后导入系统
## 注意事项
1. 示例文件仅用于测试,不包含完整的量表内容
2. 实际使用时应获取完整、正规的量表文档
3. 请注意量表的版权和使用许可
4. 建议使用格式规范的文档,以提高自动识别准确率
## 支持的文档格式
- **JSON.json** - 推荐方式,格式规范,导入准确
- **PDF.pdf** - 自动解析,可能需手动调整
- **Word 2007+.docx** - 自动解析,可能需手动调整
- **Word 2003.doc** - 自动解析,可能需手动调整
## 测试建议
1. **推荐流程**
- 首先查看 `scale-import-example.json` 了解JSON格式
- 参考 `../16-量表导入JSON标准格式说明.md` 了解详细规范
- 使用JSON格式导入最准确、最可靠
- 或使用PDF/DOCX导入后在预览界面调整为JSON格式
2. **PDF/DOCX导入建议**
- 先预览解析结果,检查识别准确性
- 如识别不准确切换到JSON文本模式手动调整
- 建议先预览再导入,确保数据准确性
3. **JSON格式导入优势**
- 格式规范,导入准确
- 可以精确控制所有字段
- 支持批量导入
- 易于版本控制和备份

View File

@ -0,0 +1,69 @@
# SCL-90量表JSON导入说明
## 📋 概述
本文档说明如何使用生成的`SCL90症状自评量表.json`文件导入完整的SCL-90量表数据。
## ✅ 自动映射支持
系统现已支持通过`factorCode`自动映射到`factorId`。**导入时会自动配置因子解释和预警规则**,无需手动配置!
## 📝 导入步骤
### 方式1使用JSON文件导入推荐
1. 进入系统 → `心理测评管理``量表管理`
2. 点击 **"导入"** 按钮
3. 切换到 **"文件上传"** 标签页
4. 选择 `SCL90症状自评量表.json` 文件
5. 点击 **"确定"** 导入
### 方式2使用JSON文本导入
1. 进入系统 → `心理测评管理``量表管理`
2. 点击 **"导入"** 按钮
3. 切换到 **"JSON文本"** 标签页
4. 复制JSON文件内容并粘贴
5. 点击 **"确定"** 导入
## 🎉 自动配置说明
系统会自动处理以下配置:
### 1. 自动配置因子解释
导入时会自动为每个因子创建4个级别的解释规则正常、轻度、中度、重度根据因子总分范围自动匹配。
### 2. 自动配置预警规则
导入时会自动创建以下预警规则:
- **总体预警**重度症状预警总分250-360和紧急症状预警总分300-360
- **因子预警**每个因子的重度预警均分≥3.0
- **敏感因子预警**F4抑郁、F5焦虑、F9精神病性的中度预警均分2.0-2.9
所有配置都会自动关联到对应的因子,无需手动配置!
## ✅ 验证清单
导入完成后,请检查:
- [ ] 量表基本信息正确(名称、编码、描述等)
- [ ] 90个题目全部导入
- [ ] 每个题目都有5个选项0-4分
- [ ] 9个因子全部导入
- [ ] 每个因子都有正确的计分规则
- [ ] 因子解释规则已自动配置
- [ ] 预警规则已自动配置
## 📚 相关文档
- [量表导入JSON标准格式说明](./16-量表导入JSON标准格式说明.md)
- [JSON格式错误检查说明](./JSON格式错误检查说明.md)
## ✨ 功能特性
- ✅ 自动映射`factorCode`到`factorId`
- ✅ 自动配置因子解释规则
- ✅ 自动配置预警规则
- ✅ 完整的错误处理和日志记录

View File

@ -0,0 +1,31 @@
# PDF转JSON转换说明
由于无法直接在此环境中运行Java代码解析PDF我将基于SCL-90标准量表结构创建一个完整的JSON文件。
## SCL-90标准结构
- **90个题目**
- **9个因子**F1躯体化、F2强迫、F3人际关系敏感、F4抑郁、F5焦虑、F6敌对、F7恐怖、F8偏执、F9精神病性
- **5级评分**0-4分没有=0很轻=1中等=2偏重=3严重=4
## 转换方法
系统已经实现了PDF解析功能您可以通过以下方式转换
### 方法1使用系统导入功能推荐
1. 登录系统
2. 进入 `心理测评管理``量表管理`
3. 点击 **"导入"** 按钮
4. 上传 `SCL90症状自评量表含常模.pdf` 文件
5. 点击 **"预览解析结果"** 查看系统自动解析的结果
6. 切换到 **"JSON文本"** 标签页
7. 复制JSON内容
8. 保存到文件
### 方法2手动创建JSON
根据SCL-90标准量表内容手动创建完整的JSON文件。
我将为您创建一个基于SCL-90标准结构的完整JSON文件。

View File

@ -0,0 +1,463 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
生成SCL-90完整JSON文件的脚本
包含90个题目9个因子计分规则结果解释和预警规则
"""
import json
# SCL-90标准90个题目内容
scl90_items = [
"头痛", "神经过敏,心中不踏实", "头脑中有不必要的想法或字句盘旋", "头昏或昏倒", "对异性的兴趣减退",
"对旁人责备求全", "感到别人能控制您的思想", "责怪别人制造麻烦", "忘记性大", "担心自己的衣饰整齐及仪态的端正",
"容易烦恼和激动", "胸痛", "害怕空旷的场所或街道", "感到自己的精力下降,活动减慢", "想结束自己的生命",
"听到旁人听不到的声音", "发抖", "感到大多数人都不可信任", "胃口不好", "容易哭泣",
"同异性相处时感到害羞不自在", "感到受骗、中了圈套或有人想抓住您", "无缘无故地突然感到害怕", "自己不能控制地大发脾气", "怕单独出门",
"经常责怪自己", "腰痛", "感到难以完成任务", "感到孤独", "感到苦闷",
"过分担忧", "对事物不感兴趣", "感到害怕", "您的感情容易受到伤害", "感到旁人能知道您的私下想法",
"感到别人不理解您、不同情您", "感到人们对您不友好、不喜欢您", "做事必须做得很慢以保证做得正确", "心跳得很厉害", "恶心或胃部不舒服",
"感到比不上他人", "肌肉酸痛", "感到有人在监视您、谈论您", "难以入睡", "做事必须反复检查",
"难以做出决定", "怕乘电车、公共汽车、地铁或火车", "呼吸有困难", "一阵阵发冷或发热", "因为感到害怕而避开某些东西、场合或活动",
"脑子变空了", "身体发麻或刺痛", "喉咙有梗塞感", "感到前途没有希望", "不能集中注意",
"感到身体的某一部分软弱无力", "感到紧张或容易紧张", "感到手或脚发重", "想到死亡的事", "吃得太多",
"当别人看着您或谈论您时感到不自在", "有一些不属于您自己的想法", "有想打人或伤害他人的冲动", "醒得太早", "必须反复洗手、点数或触摸某些东西",
"睡得不稳不深", "有想摔坏或破坏东西的冲动", "有一些别人没有的想法或念头", "感到对别人神经过敏", "在商店或电影院等人多的地方感到不自在",
"感到任何事情都很困难", "一阵阵恐惧或惊恐", "感到在公共场合吃东西很不舒服", "经常与人争论", "单独一人时神经很紧张",
"别人对您的成绩没有做出恰当的评价", "即使和别人在一起也感到孤单", "感到坐立不安心神不定", "感到自己没有什么价值", "感到熟悉的东西变成陌生或不像是真的",
"大叫或摔东西", "害怕会在公共场合昏倒", "感到别人想占您的便宜", "为一些有关\"\"的想法而很苦恼", "您认为应该因为自己的过错而受到惩罚",
"感到要很快把事情做完", "感到自己的身体有严重问题", "从未感到和其他人很亲近", "感到自己有罪", "感到自己的脑子有毛病"
]
# SCL-90因子定义因子代码、名称、包含的题目编号
factors_data = [
{"code": "F1", "name": "躯体化", "items": [1,4,12,27,40,42,48,49,52,53,56,58]},
{"code": "F2", "name": "强迫症状", "items": [3,9,10,28,38,45,46,51,55,65]},
{"code": "F3", "name": "人际关系敏感", "items": [6,21,34,36,37,41,61,69,73]},
{"code": "F4", "name": "抑郁", "items": [5,14,15,20,22,26,29,30,31,32,54,71,79]},
{"code": "F5", "name": "焦虑", "items": [2,17,23,33,39,57,72,78,80,86]},
{"code": "F6", "name": "敌对", "items": [11,24,63,67,74,81]},
{"code": "F7", "name": "恐怖", "items": [13,25,47,50,70,75,82]},
{"code": "F8", "name": "偏执", "items": [8,18,43,68,76,83]},
{"code": "F9", "name": "精神病性", "items": [7,16,35,62,77,84,85,87,88,90]}
]
# 因子英文名称映射
factor_en_names = {
"F1": "Somatization",
"F2": "Obsessive-Compulsive",
"F3": "Interpersonal Sensitivity",
"F4": "Depression",
"F5": "Anxiety",
"F6": "Hostility",
"F7": "Phobic Anxiety",
"F8": "Paranoid Ideation",
"F9": "Psychoticism"
}
# 生成量表基本信息
scale_info = {
"scaleCode": "SCL_90",
"scaleName": "症状自评量表SCL-90",
"scaleEnName": "Symptom Checklist 90",
"scaleType": "symptom",
"scaleVersion": "1.0",
"scaleIntro": "SCL-90是一个包含90个项目的症状自评量表用于评估个体的心理症状水平",
"scaleDescription": "SCL-90量表包含90个项目涵盖9个症状因子躯体化、强迫症状、人际关系敏感、抑郁、焦虑、敌对、恐怖、偏执、精神病性。采用5级评分0-4分是心理健康评估的重要工具。",
"itemCount": 90,
"estimatedTime": 30,
"targetPopulation": "一般人群",
"author": "Derogatis",
"source": "心理卫生评定量表手册",
"reference": "Derogatis, L. R. (1977). SCL-90: Administration, scoring, and procedures manual. Clinical Psychometric Research.",
"status": "0",
"sortOrder": 0
}
# 生成题目列表(包含选项)
items_list = []
for i, content in enumerate(scl90_items, 1):
item_data = {
"item": {
"itemNumber": i,
"itemContent": content,
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": i
},
"options": [
{"optionCode": "A", "optionContent": "没有", "optionScore": 0, "sortOrder": 1},
{"optionCode": "B", "optionContent": "很轻", "optionScore": 1, "sortOrder": 2},
{"optionCode": "C", "optionContent": "中等", "optionScore": 2, "sortOrder": 3},
{"optionCode": "D", "optionContent": "偏重", "optionScore": 3, "sortOrder": 4},
{"optionCode": "E", "optionContent": "严重", "optionScore": 4, "sortOrder": 5}
]
}
items_list.append(item_data)
# 生成因子列表(包含计分规则)
factors_list = []
for idx, factor_data in enumerate(factors_data, 1):
factor_obj = {
"factor": {
"factorCode": factor_data["code"],
"factorName": factor_data["name"],
"factorEnName": factor_en_names.get(factor_data["code"], ""),
"factorDescription": "SCL-90 {}因子,包含{}个题目".format(factor_data["name"], len(factor_data["items"])),
"factorOrder": idx
},
"rules": [
{
"itemNumber": item_num,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
for item_num in factor_data["items"]
]
}
factors_list.append(factor_obj)
# SCL-90因子常模和解释规则
# 因子均分标准:<1.0正常1.0-1.9轻度2.0-2.9中度≥3.0重度
# 因子总分 = 因子均分 × 题目数量
# 因子题目数量和总分范围
factor_ranges = {
"F1": {"itemCount": 12, "maxScore": 48}, # 躯体化
"F2": {"itemCount": 10, "maxScore": 40}, # 强迫症状
"F3": {"itemCount": 9, "maxScore": 36}, # 人际关系敏感
"F4": {"itemCount": 13, "maxScore": 52}, # 抑郁
"F5": {"itemCount": 10, "maxScore": 40}, # 焦虑
"F6": {"itemCount": 6, "maxScore": 24}, # 敌对
"F7": {"itemCount": 7, "maxScore": 28}, # 恐怖
"F8": {"itemCount": 6, "maxScore": 24}, # 偏执
"F9": {"itemCount": 10, "maxScore": 40} # 精神病性
}
# 生成总体解释配置总分范围0-360
interpretations_list = [
{
"factorId": None,
"scoreRangeMin": 0,
"scoreRangeMax": 160,
"level": "",
"levelName": "正常",
"interpretationTitle": "正常范围",
"interpretationContent": "您的总体得分在正常范围内,心理健康状况良好。",
"suggestions": "继续保持良好的心理状态,注意日常生活中的压力管理。",
"sortOrder": 1
},
{
"factorId": None,
"scoreRangeMin": 161,
"scoreRangeMax": 250,
"level": "",
"levelName": "轻度",
"interpretationTitle": "轻度症状",
"interpretationContent": "您可能存在轻度的心理症状,建议适当关注自己的心理健康。",
"suggestions": "建议通过放松训练、运动、社交等方式缓解压力,如症状持续可考虑咨询专业人士。",
"sortOrder": 2
},
{
"factorId": None,
"scoreRangeMin": 251,
"scoreRangeMax": 360,
"level": "",
"levelName": "中重度",
"interpretationTitle": "中重度症状",
"interpretationContent": "您的得分显示可能存在中重度的心理症状,建议寻求专业心理帮助。",
"suggestions": "强烈建议咨询心理医生或心理治疗师,进行专业的心理评估和治疗。",
"sortOrder": 3
}
]
# 为每个因子生成解释配置(基于因子总分)
# 因子均分 < 1.0 为正常1.0-1.9 为轻度2.0-2.9 为中度,≥ 3.0 为重度
factor_interpretations = {
"F1": {
"name": "躯体化",
"descriptions": {
"正常": "您的躯体化因子得分在正常范围内,躯体不适感较轻。",
"轻度": "您可能存在轻度的躯体化症状,建议关注身体健康状况。",
"中度": "您存在中度的躯体化症状,建议进行身体检查并关注心理健康。",
"重度": "您存在重度的躯体化症状,强烈建议寻求医疗和心理双重帮助。"
},
"suggestions": {
"正常": "继续保持良好的生活习惯,定期体检。",
"轻度": "建议关注身体健康,适当进行体检,同时注意心理压力的管理。",
"中度": "建议进行全面的身体检查,排除器质性病变,并寻求心理帮助。",
"重度": "强烈建议尽快就医,进行全面的身体和心理评估,制定综合治疗方案。"
}
},
"F2": {
"name": "强迫症状",
"descriptions": {
"正常": "您的强迫症状因子得分在正常范围内,强迫思维和行为较少。",
"轻度": "您可能存在轻度的强迫症状,偶尔出现不必要的想法或行为。",
"中度": "您存在中度的强迫症状,可能影响日常生活和工作效率。",
"重度": "您存在重度的强迫症状,严重影响日常生活,需要专业治疗。"
},
"suggestions": {
"正常": "继续保持良好的心理状态。",
"轻度": "建议学习放松技巧,减少不必要的担心和重复行为。",
"中度": "建议寻求心理治疗,学习认知行为疗法等方法来缓解强迫症状。",
"重度": "强烈建议寻求专业心理治疗,可能需要药物治疗配合心理治疗。"
}
},
"F3": {
"name": "人际关系敏感",
"descriptions": {
"正常": "您的人际关系敏感因子得分在正常范围内,人际交往较为自然。",
"轻度": "您可能在人际交往中偶尔感到不适或敏感。",
"中度": "您存在中度的人际关系敏感,可能影响社交能力和人际关系。",
"重度": "您存在重度的人际关系敏感,严重影响社交和人际关系,需要帮助。"
},
"suggestions": {
"正常": "继续保持良好的人际交往。",
"轻度": "建议增加社交活动,培养自信,学习沟通技巧。",
"中度": "建议寻求心理咨询,学习改善人际关系的方法和技巧。",
"重度": "强烈建议寻求专业心理治疗,改善社交恐惧和人际关系问题。"
}
},
"F4": {
"name": "抑郁",
"descriptions": {
"正常": "您的抑郁因子得分在正常范围内,情绪状态良好。",
"轻度": "您可能存在轻度的抑郁情绪,偶尔感到沮丧或失落。",
"中度": "您存在中度的抑郁症状,可能影响日常生活和工作。",
"重度": "您存在重度的抑郁症状,严重影响生活功能,需要立即寻求帮助。"
},
"suggestions": {
"正常": "继续保持良好的情绪状态。",
"轻度": "建议增加运动,培养兴趣爱好,保持规律的作息。",
"中度": "建议寻求心理咨询或心理治疗,必要时考虑药物治疗。",
"重度": "强烈建议立即寻求专业帮助,可能需要药物治疗和心理治疗相结合。"
}
},
"F5": {
"name": "焦虑",
"descriptions": {
"正常": "您的焦虑因子得分在正常范围内,焦虑水平较低。",
"轻度": "您可能存在轻度的焦虑情绪,偶尔感到紧张或担心。",
"中度": "您存在中度的焦虑症状,可能影响日常生活和工作。",
"重度": "您存在重度的焦虑症状,严重影响生活功能,需要专业治疗。"
},
"suggestions": {
"正常": "继续保持良好的心理状态。",
"轻度": "建议学习放松技巧,进行深呼吸和冥想练习。",
"中度": "建议寻求心理咨询,学习焦虑管理技巧,必要时考虑药物治疗。",
"重度": "强烈建议寻求专业治疗,可能需要药物治疗配合心理治疗。"
}
},
"F6": {
"name": "敌对",
"descriptions": {
"正常": "您的敌对因子得分在正常范围内,情绪控制良好。",
"轻度": "您可能存在轻度的敌对情绪,偶尔感到愤怒或烦躁。",
"中度": "您存在中度的敌对情绪,可能影响人际关系。",
"重度": "您存在重度的敌对情绪,严重影响人际关系和社会功能。"
},
"suggestions": {
"正常": "继续保持良好的情绪管理。",
"轻度": "建议学习情绪管理技巧,进行适当的运动来释放压力。",
"中度": "建议寻求心理咨询,学习愤怒管理和冲突解决技巧。",
"重度": "强烈建议寻求专业心理治疗,改善情绪控制和人际交往能力。"
}
},
"F7": {
"name": "恐怖",
"descriptions": {
"正常": "您的恐怖因子得分在正常范围内,恐惧情绪较少。",
"轻度": "您可能存在轻度的恐怖情绪,对某些情境感到轻微不安。",
"中度": "您存在中度的恐怖情绪,可能影响正常生活和工作。",
"重度": "您存在重度的恐怖情绪,严重影响正常生活,需要专业治疗。"
},
"suggestions": {
"正常": "继续保持良好的心理状态。",
"轻度": "建议逐步面对恐惧,进行脱敏训练。",
"中度": "建议寻求心理咨询,进行系统脱敏治疗。",
"重度": "强烈建议寻求专业心理治疗,可能需要暴露疗法和药物治疗。"
}
},
"F8": {
"name": "偏执",
"descriptions": {
"正常": "您的偏执因子得分在正常范围内,信任感良好。",
"轻度": "您可能存在轻度的偏执倾向,偶尔对他人产生怀疑。",
"中度": "您存在中度的偏执倾向,可能影响人际关系和信任。",
"重度": "您存在重度的偏执倾向,严重影响人际关系和社会功能。"
},
"suggestions": {
"正常": "继续保持良好的人际信任。",
"轻度": "建议增强自信,学习信任他人,改善人际关系。",
"中度": "建议寻求心理咨询,改善偏执思维,学习正确的认知方式。",
"重度": "强烈建议寻求专业心理治疗,可能需要认知行为疗法和药物治疗。"
}
},
"F9": {
"name": "精神病性",
"descriptions": {
"正常": "您的精神病性因子得分在正常范围内,思维清晰。",
"轻度": "您可能存在轻微的精神病性症状,偶尔出现异常想法。",
"中度": "您存在中度的精神病性症状,可能影响思维和判断。",
"重度": "您存在重度的精神病性症状,严重影响思维功能,需要立即就医。"
},
"suggestions": {
"正常": "继续保持良好的心理状态。",
"轻度": "建议关注心理健康,如有持续异常思维请及时咨询。",
"中度": "强烈建议寻求精神科医生的专业评估和治疗。",
"重度": "紧急建议立即寻求精神科专业治疗,可能需要住院治疗。"
}
}
}
# 为每个因子添加解释配置
for factor_code, factor_info in factor_interpretations.items():
if factor_code in factor_ranges:
item_count = factor_ranges[factor_code]["itemCount"]
max_score = factor_ranges[factor_code]["maxScore"]
# 正常:均分 < 1.0,总分 < 题目数
interpretations_list.append({
"factorId": None, # 导入时会通过factorCode映射设置
"factorCode": factor_code, # 用于映射到factorId的临时字段
"scoreRangeMin": 0,
"scoreRangeMax": item_count - 1,
"level": "",
"levelName": "正常",
"interpretationTitle": "{}因子正常".format(factor_info["name"]),
"interpretationContent": factor_info["descriptions"]["正常"],
"suggestions": factor_info["suggestions"]["正常"],
"sortOrder": 1
})
# 轻度:均分 1.0-1.9,总分 = 题目数 × 1.0 到 题目数 × 1.9
min_light = item_count * 1.0
max_light = int(item_count * 1.9)
interpretations_list.append({
"factorId": None,
"factorCode": factor_code,
"scoreRangeMin": int(min_light),
"scoreRangeMax": max_light,
"level": "",
"levelName": "轻度",
"interpretationTitle": "{}因子轻度".format(factor_info["name"]),
"interpretationContent": factor_info["descriptions"]["轻度"],
"suggestions": factor_info["suggestions"]["轻度"],
"sortOrder": 2
})
# 中度:均分 2.0-2.9,总分 = 题目数 × 2.0 到 题目数 × 2.9
min_moderate = item_count * 2.0
max_moderate = int(item_count * 2.9)
interpretations_list.append({
"factorId": None,
"factorCode": factor_code,
"scoreRangeMin": int(min_moderate),
"scoreRangeMax": max_moderate,
"level": "",
"levelName": "中度",
"interpretationTitle": "{}因子中度".format(factor_info["name"]),
"interpretationContent": factor_info["descriptions"]["中度"],
"suggestions": factor_info["suggestions"]["中度"],
"sortOrder": 3
})
# 重度:均分 ≥ 3.0,总分 ≥ 题目数 × 3.0
min_severe = int(item_count * 3.0)
interpretations_list.append({
"factorId": None,
"factorCode": factor_code,
"scoreRangeMin": min_severe,
"scoreRangeMax": max_score,
"level": "",
"levelName": "重度",
"interpretationTitle": "{}因子重度".format(factor_info["name"]),
"interpretationContent": factor_info["descriptions"]["重度"],
"suggestions": factor_info["suggestions"]["重度"],
"sortOrder": 4
})
# 生成预警规则
warning_rules_list = [
# 总体预警规则
{
"factorId": None,
"ruleName": "重度症状预警",
"warningLevel": "",
"scoreMin": 250,
"scoreMax": 360,
"autoRelief": "0",
"status": "0"
},
{
"factorId": None,
"ruleName": "紧急症状预警",
"warningLevel": "紧急",
"scoreMin": 300,
"scoreMax": 360,
"autoRelief": "0",
"status": "0"
}
]
# 为每个因子添加预警规则(重度及以上需要预警)
for factor_code, factor_info in factor_interpretations.items():
if factor_code in factor_ranges:
item_count = factor_ranges[factor_code]["itemCount"]
max_score = factor_ranges[factor_code]["maxScore"]
# 重度预警(均分 ≥ 3.0
min_severe = int(item_count * 3.0)
warning_rules_list.append({
"factorId": None, # 导入时会通过factorCode映射设置
"factorCode": factor_code, # 用于映射到factorId的临时字段
"ruleName": "{}因子重度预警".format(factor_info["name"]),
"warningLevel": "",
"scoreMin": min_severe,
"scoreMax": max_score,
"autoRelief": "0",
"status": "0"
})
# 中度预警(均分 2.0-2.9- 对于某些敏感因子
if factor_code in ["F4", "F5", "F9"]: # 抑郁、焦虑、精神病性需要中度预警
min_moderate = int(item_count * 2.0)
max_moderate = int(item_count * 2.9)
warning_rules_list.append({
"factorId": None,
"factorCode": factor_code,
"ruleName": "{}因子中度预警".format(factor_info["name"]),
"warningLevel": "",
"scoreMin": min_moderate,
"scoreMax": max_moderate,
"autoRelief": "0",
"status": "0"
})
# 组合完整的JSON对象
json_data = {
"scale": scale_info,
"items": items_list,
"factors": factors_list,
"interpretations": interpretations_list,
"warningRules": warning_rules_list
}
# 输出JSON文件
output_file = "SCL90症状自评量表.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=2)
print("✅ SCL-90 JSON文件已生成: {}".format(output_file))
print("📊 统计信息:")
print(" - 量表: {}".format(scale_info["scaleName"]))
print(" - 题目数量: {}".format(len(items_list)))
print(" - 因子数量: {}".format(len(factors_list)))
print(" - 结果解释: {}".format(len(interpretations_list)))
print(" - 预警规则: {}".format(len(warning_rules_list)))

View File

@ -0,0 +1,124 @@
// 生成SCL-90完整JSON文件的脚本
// SCL-90标准90个题目内容
const scl90Items = [
"头痛", "神经过敏,心中不踏实", "头脑中有不必要的想法或字句盘旋", "头昏或昏倒", "对异性的兴趣减退",
"对旁人责备求全", "感到别人能控制您的思想", "责怪别人制造麻烦", "忘记性大", "担心自己的衣饰整齐及仪态的端正",
"容易烦恼和激动", "胸痛", "害怕空旷的场所或街道", "感到自己的精力下降,活动减慢", "想结束自己的生命",
"听到旁人听不到的声音", "发抖", "感到大多数人都不可信任", "胃口不好", "容易哭泣",
"同异性相处时感到害羞不自在", "感到受骗、中了圈套或有人想抓住您", "无缘无故地 suddenly 感到害怕", "自己不能控制地大发脾气", "怕单独出门",
"经常责怪自己", "腰痛", "感到难以完成任务", "感到孤独", "感到苦闷",
"过分担忧", "对事物不感兴趣", "感到害怕", "您的感情容易受到伤害", "感到旁人能知道您的私下想法",
"感到别人不理解您、不同情您", "感到人们对您不友好、不喜欢您", "做事必须做得很慢以保证做得正确", "心跳得很厉害", "恶心或胃部不舒服",
"感到比不上他人", "肌肉酸痛", "感到有人在监视您、谈论您", "难以入睡", "做事必须反复检查",
"难以做出决定", "怕乘电车、公共汽车、地铁或火车", "呼吸有困难", "一阵阵发冷或发热", "因为感到害怕而避开某些东西、场合或活动",
"脑子变空了", "身体发麻或刺痛", "喉咙有梗塞感", "感到前途没有希望", "不能集中注意",
"感到身体的某一部分软弱无力", "感到紧张或容易紧张", "感到手或脚发重", "想到死亡的事", "吃得太多",
"当别人看着您或谈论您时感到不自在", "有一些不属于您自己的想法", "有想打人或伤害他人的冲动", "醒得太早", "必须反复洗手、点数或触摸某些东西",
"睡得不稳不深", "有想摔坏或破坏东西的冲动", "有一些别人没有的想法或念头", "感到对别人神经过敏", "在商店或电影院等人多的地方感到不自在",
"感到任何事情都很困难", "一阵阵恐惧或惊恐", "感到在公共场合吃东西很不舒服", "经常与人争论", "单独一人时神经很紧张",
"别人对您的成绩没有做出恰当的评价", "即使和别人在一起也感到孤单", "感到坐立不安心神不定", "感到自己没有什么价值", "感到熟悉的东西变成陌生或不像是真的",
"大叫或摔东西", "害怕会在公共场合昏倒", "感到别人想占您的便宜", "为一些有关""的想法而很苦恼", "您认为应该因为自己的过错而受到惩罚",
"感到要很快把事情做完", "感到自己的身体有严重问题", "从未感到和其他人很亲近", "感到自己有罪", "感到自己的脑子有毛病"
];
// SCL-90因子定义
const factors = [
{ code: "F1", name: "躯体化", items: [1,4,12,27,40,42,48,49,52,53,56,58] },
{ code: "F2", name: "强迫症状", items: [3,9,10,28,38,45,46,51,55,65] },
{ code: "F3", name: "人际关系敏感", items: [6,21,34,36,37,41,61,69,73] },
{ code: "F4", name: "抑郁", items: [5,14,15,20,22,26,29,30,31,32,54,71,79] },
{ code: "F5", name: "焦虑", items: [2,17,23,33,39,57,72,78,80,86] },
{ code: "F6", name: "敌对", items: [11,24,63,67,74,81] },
{ code: "F7", name: "恐怖", items: [13,25,47,50,70,75,82] },
{ code: "F8", name: "偏执", items: [8,18,43,68,76,83] },
{ code: "F9", name: "精神病性", items: [7,16,35,62,77,84,85,87,88,90] }
];
// 生成JSON
const json = {
scale: {
scaleCode: "SCL_90",
scaleName: "症状自评量表SCL-90",
scaleEnName: "Symptom Checklist 90",
scaleType: "symptom",
scaleVersion: "1.0",
scaleIntro: "SCL-90是一个包含90个项目的症状自评量表用于评估个体的心理症状水平",
scaleDescription: "SCL-90量表包含90个项目涵盖9个症状因子躯体化、强迫症状、人际关系敏感、抑郁、焦虑、敌对、恐怖、偏执、精神病性。采用5级评分0-4分是心理健康评估的重要工具。",
itemCount: 90,
estimatedTime: 30,
targetPopulation: "一般人群",
author: "Derogatis",
source: "心理卫生评定量表手册",
reference: "Derogatis, L. R. (1977). SCL-90: Administration, scoring, and procedures manual. Clinical Psychometric Research.",
status: "0",
sortOrder: 0
},
items: scl90Items.map((content, index) => ({
item: {
itemNumber: index + 1,
itemContent: content,
itemType: "single",
required: "1",
reverseScore: "0",
sortOrder: index + 1
},
options: [
{ optionCode: "A", optionContent: "没有", optionScore: 0, sortOrder: 1 },
{ optionCode: "B", optionContent: "很轻", optionScore: 1, sortOrder: 2 },
{ optionCode: "C", optionContent: "中等", optionScore: 2, sortOrder: 3 },
{ optionCode: "D", optionContent: "偏重", optionScore: 3, sortOrder: 4 },
{ optionCode: "E", optionContent: "严重", optionScore: 4, sortOrder: 5 }
]
})),
factors: factors.map((factor, index) => ({
factor: {
factorCode: factor.code,
factorName: factor.name,
factorEnName: getFactorEnName(factor.code),
factorDescription: `SCL-90 ${factor.name}因子,包含${factor.items.length}个题目`,
factorOrder: index + 1
},
rules: factor.items.map(itemNum => ({
itemNumber: itemNum,
rule: {
optionIds: "",
weight: 1.0,
calculationType: "sum"
}
}))
})),
interpretations: [
// 总体解释
{ factorId: null, scoreRangeMin: 0, scoreRangeMax: 160, level: "低", levelName: "正常", interpretationTitle: "正常范围", interpretationContent: "您的总体得分在正常范围内,心理健康状况良好。", suggestions: "继续保持良好的心理状态,注意日常生活中的压力管理。", sortOrder: 1 },
{ factorId: null, scoreRangeMin: 161, scoreRangeMax: 250, level: "中", levelName: "轻度", interpretationTitle: "轻度症状", interpretationContent: "您可能存在轻度的心理症状,建议适当关注自己的心理健康。", suggestions: "建议通过放松训练、运动、社交等方式缓解压力,如症状持续可考虑咨询专业人士。", sortOrder: 2 },
{ factorId: null, scoreRangeMin: 251, scoreRangeMax: 360, level: "高", levelName: "中重度", interpretationTitle: "中重度症状", interpretationContent: "您的得分显示可能存在中重度的心理症状,建议寻求专业心理帮助。", suggestions: "强烈建议咨询心理医生或心理治疗师,进行专业的心理评估和治疗。", sortOrder: 3 }
],
warningRules: [
{ factorId: null, ruleName: "重度症状预警", warningLevel: "高", scoreMin: 250, scoreMax: 360, autoRelief: "0", status: "0" },
{ factorId: null, ruleName: "紧急症状预警", warningLevel: "紧急", scoreMin: 300, scoreMax: 360, autoRelief: "0", status: "0" }
]
};
function getFactorEnName(code) {
const names = {
"F1": "Somatization",
"F2": "Obsessive-Compulsive",
"F3": "Interpersonal Sensitivity",
"F4": "Depression",
"F5": "Anxiety",
"F6": "Hostility",
"F7": "Phobic Anxiety",
"F8": "Paranoid Ideation",
"F9": "Psychoticism"
};
return names[code] || "";
}
// 输出JSON需要Node.js环境运行
if (typeof module !== 'undefined' && module.exports) {
module.exports = json;
} else {
console.log(JSON.stringify(json, null, 2));
}

View File

@ -0,0 +1,388 @@
{
"scale": {
"scaleCode": "SCL_90_EXAMPLE",
"scaleName": "症状自评量表SCL-90示例",
"scaleEnName": "Symptom Checklist 90",
"scaleType": "symptom",
"scaleVersion": "1.0",
"scaleIntro": "SCL-90是一个包含90个项目的症状自评量表用于评估个体的心理症状水平",
"scaleDescription": "SCL-90量表包含90个项目涵盖9个症状因子是心理健康评估的重要工具",
"itemCount": 90,
"estimatedTime": 30,
"targetPopulation": "一般人群",
"author": "Derogatis",
"source": "心理卫生评定量表手册",
"reference": "Derogatis, L. R. (1977). SCL-90: Administration, scoring, and procedures manual. Clinical Psychometric Research.",
"status": "0",
"sortOrder": 0
},
"items": [
{
"item": {
"itemNumber": 1,
"itemContent": "头痛",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 1
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
},
{
"item": {
"itemNumber": 2,
"itemContent": "神经过敏,心中不踏实",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 2
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
},
{
"item": {
"itemNumber": 3,
"itemContent": "头脑中有不必要的想法或字句盘旋",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 3
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
},
{
"item": {
"itemNumber": 4,
"itemContent": "头昏或昏倒",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 4
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
},
{
"item": {
"itemNumber": 5,
"itemContent": "对异性的兴趣减退",
"itemType": "single",
"required": "1",
"reverseScore": "0",
"sortOrder": 5
},
"options": [
{
"optionCode": "A",
"optionContent": "没有",
"optionScore": 0,
"sortOrder": 1
},
{
"optionCode": "B",
"optionContent": "很轻",
"optionScore": 1,
"sortOrder": 2
},
{
"optionCode": "C",
"optionContent": "中等",
"optionScore": 2,
"sortOrder": 3
},
{
"optionCode": "D",
"optionContent": "偏重",
"optionScore": 3,
"sortOrder": 4
},
{
"optionCode": "E",
"optionContent": "严重",
"optionScore": 4,
"sortOrder": 5
}
]
}
],
"factors": [
{
"factor": {
"factorCode": "F1",
"factorName": "躯体化",
"factorEnName": "Somatization",
"factorDescription": "反映躯体不适感,包括心血管、胃肠道、呼吸系统不适和头痛、背痛、肌肉酸痛等",
"factorOrder": 1
},
"rules": [
{
"itemNumber": 1,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
},
{
"itemNumber": 4,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
},
{
"factor": {
"factorCode": "F2",
"factorName": "强迫症状",
"factorEnName": "Obsessive-Compulsive",
"factorDescription": "反映强迫思维和强迫行为",
"factorOrder": 2
},
"rules": [
{
"itemNumber": 3,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
},
{
"factor": {
"factorCode": "F4",
"factorName": "抑郁",
"factorEnName": "Depression",
"factorDescription": "反映与临床抑郁症状群相联系的广泛概念",
"factorOrder": 4
},
"rules": [
{
"itemNumber": 5,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
},
{
"factor": {
"factorCode": "F5",
"factorName": "焦虑",
"factorEnName": "Anxiety",
"factorDescription": "反映与焦虑症状相联系的精神症状及体验",
"factorOrder": 5
},
"rules": [
{
"itemNumber": 2,
"rule": {
"optionIds": "",
"weight": 1.0,
"calculationType": "sum"
}
}
]
}
],
"interpretations": [
{
"scoreMin": 0,
"scoreMax": 160,
"level": "低",
"levelName": "正常",
"interpretationTitle": "正常范围",
"interpretationContent": "您的总体得分在正常范围内,心理健康状况良好。",
"suggestions": "继续保持良好的心理状态,注意日常生活中的压力管理。",
"sortOrder": 1
},
{
"scoreMin": 161,
"scoreMax": 250,
"level": "中",
"levelName": "轻度",
"interpretationTitle": "轻度症状",
"interpretationContent": "您可能存在轻度的心理症状,建议适当关注自己的心理健康。",
"suggestions": "建议通过放松训练、运动、社交等方式缓解压力,如症状持续可考虑咨询专业人士。",
"sortOrder": 2
},
{
"scoreMin": 251,
"scoreMax": 360,
"level": "高",
"levelName": "中重度",
"interpretationTitle": "中重度症状",
"interpretationContent": "您的得分显示可能存在中重度的心理症状,建议寻求专业心理帮助。",
"suggestions": "强烈建议咨询心理医生或心理治疗师,进行专业的心理评估和治疗。",
"sortOrder": 3
},
{
"factorId": null,
"scoreMin": 0,
"scoreMax": 10,
"level": "低",
"levelName": "正常",
"interpretationTitle": "躯体化因子正常",
"interpretationContent": "您的躯体化因子得分在正常范围内。",
"suggestions": "",
"sortOrder": 1
},
{
"factorId": null,
"scoreMin": 11,
"scoreMax": 20,
"level": "中",
"levelName": "轻度",
"interpretationTitle": "躯体化因子轻度",
"interpretationContent": "您可能存在轻度的躯体化症状。",
"suggestions": "建议关注身体健康,必要时进行体检。",
"sortOrder": 2
}
],
"warningRules": [
{
"ruleName": "重度症状预警",
"warningLevel": "高",
"scoreMin": 250,
"scoreMax": 360,
"status": "0"
},
{
"ruleName": "紧急症状预警",
"warningLevel": "紧急",
"scoreMin": 300,
"scoreMax": 360,
"status": "0"
}
]
}

View File

@ -0,0 +1,68 @@
# 量表文件下载说明
## 推荐的下载来源
### 1. 优路教育(免费资源)
**网址**https://www.youlu.com/ziliao/detail/pack-52513
**包含的量表**
- SCL90症状自评量表含常模.pdf
- 焦虑自评量表.pdf
- 自尊量表.pdf
- PSTR心理压力量表.pdf
- 青少年上网成瘾自评量表.pdf
**下载步骤**
1. 访问上述网址
2. 注册/登录账号(可能需要)
3. 点击"下载资料包"或单个文件下载
4. 将下载的PDF文件保存到本文件夹
### 2. 其他推荐来源
#### 心理测量相关网站
- 心理测量网
- 中国心理卫生协会官网
- 各高校心理学系网站
#### 电子书资源
- 《心理卫生评定量表手册(增订版)》(汪向东等编著)
- 《常用心理评估量表手册》
- 各大学图书馆数据库
#### 学术数据库
- 中国知网CNKI
- 万方数据
- 维普资讯
## 下载后的操作
1. **文件命名**:建议使用清晰的命名,如:
- `SCL-90症状自评量表.pdf`
- `SAS焦虑自评量表.pdf`
- `SDS抑郁自评量表.pdf`
2. **文件位置**将下载的PDF或DOCX文件保存到本文件夹`z_Project change/量表示例/`
3. **测试导入**
- 登录系统,进入"量表管理"
- 点击"导入"按钮
- 选择"文件上传"标签页
- 上传PDF或DOCX文件
- 使用"预览解析结果"查看识别效果
- 根据需要进行调整后导入
## 注意事项
1. **版权问题**:请确保您有权使用下载的量表文件
2. **文件格式**建议使用PDF格式识别准确率较高
3. **文档质量**清晰的扫描版或原生PDF文件识别效果更好
4. **内容完整性**:确保文档包含完整的题目和选项
## 测试建议
1. 先下载1-2个简单的量表如SAS、SDS进行测试
2. 熟悉导入流程后再批量导入
3. 对识别不准确的部分,可在预览界面手动调整
4. 建议每次导入后检查数据完整性