管理员能管理用户,用户也能选择量表进行测量,然后也能出现结果。但是所有操作都是在管理员的界面操作的
This commit is contained in:
parent
8ec233f820
commit
ed835d628c
|
|
@ -10,5 +10,4 @@ set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512
|
||||||
|
|
||||||
java -jar %JAVA_OPTS% ry-news-admin.jar
|
java -jar %JAVA_OPTS% ry-news-admin.jar
|
||||||
|
|
||||||
cd bin
|
|
||||||
pause
|
pause
|
||||||
|
|
@ -84,3 +84,19 @@ export function delAssessment(assessmentIds) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交测评
|
||||||
|
export function submitAssessment(assessmentId) {
|
||||||
|
return request({
|
||||||
|
url: '/psychology/assessment/submit/' + assessmentId,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取测评的答案列表
|
||||||
|
export function getAssessmentAnswers(assessmentId) {
|
||||||
|
return request({
|
||||||
|
url: '/psychology/assessment/answers/' + assessmentId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,31 @@ export const constantRoutes = [
|
||||||
|
|
||||||
// 动态路由,基于用户权限动态去加载
|
// 动态路由,基于用户权限动态去加载
|
||||||
export const dynamicRoutes = [
|
export const dynamicRoutes = [
|
||||||
|
// 系统管理菜单
|
||||||
|
{
|
||||||
|
path: '/system',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/system/user',
|
||||||
|
name: 'System',
|
||||||
|
meta: {
|
||||||
|
title: '系统管理',
|
||||||
|
icon: 'system',
|
||||||
|
roles: ['admin']
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// 菜单去重工具
|
||||||
|
{
|
||||||
|
path: 'menu/cleanup',
|
||||||
|
name: 'MenuCleanup',
|
||||||
|
component: () => import('@/views/system/menu/menuCleanup'),
|
||||||
|
meta: {
|
||||||
|
title: '菜单去重工具',
|
||||||
|
icon: 'edit',
|
||||||
|
roles: ['admin']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/system/user-auth',
|
path: '/system/user-auth',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|
@ -262,6 +287,17 @@ export const dynamicRoutes = [
|
||||||
roles: ['admin']
|
roles: ['admin']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 报告详情
|
||||||
|
{
|
||||||
|
path: 'report/detail',
|
||||||
|
name: 'ReportDetail',
|
||||||
|
component: () => import('@/views/psychology/report/detail'),
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
title: '报告详情',
|
||||||
|
roles: ['admin']
|
||||||
|
}
|
||||||
|
},
|
||||||
// 解释配置
|
// 解释配置
|
||||||
{
|
{
|
||||||
path: 'interpretation',
|
path: 'interpretation',
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { startAssessment, pausedAssessmentList } from "@/api/psychology/assessment";
|
import { startAssessment, pausedAssessmentList, resumeAssessment } from "@/api/psychology/assessment";
|
||||||
import { listScale } from "@/api/psychology/scale";
|
import { listScale } from "@/api/psychology/scale";
|
||||||
import { listProfile } from "@/api/psychology/profile";
|
import { listProfile } from "@/api/psychology/profile";
|
||||||
|
|
||||||
|
|
@ -183,7 +183,18 @@ export default {
|
||||||
},
|
},
|
||||||
/** 继续测评 */
|
/** 继续测评 */
|
||||||
handleContinue(row) {
|
handleContinue(row) {
|
||||||
this.$router.push({ path: 'assessment/taking', query: { assessmentId: row.assessmentId } });
|
// 如果测评状态是暂停,先恢复测评状态
|
||||||
|
if (row.status === '3') {
|
||||||
|
resumeAssessment(row.assessmentId).then(() => {
|
||||||
|
this.$router.push({ path: '/psychology/assessment/taking', query: { assessmentId: row.assessmentId } });
|
||||||
|
}).catch(() => {
|
||||||
|
// 即使恢复失败,也允许继续测评
|
||||||
|
this.$router.push({ path: '/psychology/assessment/taking', query: { assessmentId: row.assessmentId } });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 直接跳转到测评页面
|
||||||
|
this.$router.push({ path: '/psychology/assessment/taking', query: { assessmentId: row.assessmentId } });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/** 返回 */
|
/** 返回 */
|
||||||
handleBack() {
|
handleBack() {
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,15 @@
|
||||||
<div class="navigation-buttons">
|
<div class="navigation-buttons">
|
||||||
<el-button @click="handlePrev" :disabled="currentIndex === 0">上一题</el-button>
|
<el-button @click="handlePrev" :disabled="currentIndex === 0">上一题</el-button>
|
||||||
<el-button type="primary" @click="handleNext" :disabled="currentIndex === totalItems - 1">下一题</el-button>
|
<el-button type="primary" @click="handleNext" :disabled="currentIndex === totalItems - 1">下一题</el-button>
|
||||||
<el-button type="success" @click="handleSubmit" :disabled="!isComplete" style="margin-left: 20px;">提交测评</el-button>
|
<el-button type="success" @click="handleSubmit" :disabled="!isComplete || loading" style="margin-left: 20px;">
|
||||||
|
提交测评 ({{ answeredCount }}/{{ totalItems }})
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getAssessment, pauseAssessment, getAssessmentItems } from "@/api/psychology/assessment";
|
import { getAssessment, pauseAssessment, getAssessmentItems, submitAssessment, getAssessmentAnswers } from "@/api/psychology/assessment";
|
||||||
import { listOption } from "@/api/psychology/option";
|
import { listOption } from "@/api/psychology/option";
|
||||||
import { saveAnswer } from "@/api/psychology/assessment";
|
import { saveAnswer } from "@/api/psychology/assessment";
|
||||||
|
|
||||||
|
|
@ -99,8 +101,27 @@ export default {
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
return this.totalItems > 0 ? Math.round((this.currentIndex + 1) / this.totalItems * 100) : 0;
|
return this.totalItems > 0 ? Math.round((this.currentIndex + 1) / this.totalItems * 100) : 0;
|
||||||
},
|
},
|
||||||
|
answeredCount() {
|
||||||
|
// 计算已答题数量
|
||||||
|
return this.itemList.filter(item => {
|
||||||
|
const answer = this.answersMap[item.itemId];
|
||||||
|
if (!answer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 单选题需要 optionId,多选题需要 optionIds(字符串,用逗号分隔)
|
||||||
|
if (item.itemType === 'single') {
|
||||||
|
return answer.optionId != null;
|
||||||
|
} else if (item.itemType === 'multiple') {
|
||||||
|
return answer.optionIds != null && answer.optionIds.trim().length > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).length;
|
||||||
|
},
|
||||||
isComplete() {
|
isComplete() {
|
||||||
return this.itemList.length > 0 && this.answersMap.size === this.itemList.length;
|
if (this.itemList.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.answeredCount === this.itemList.length;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -118,16 +139,50 @@ export default {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getAssessment(this.assessmentId),
|
getAssessment(this.assessmentId),
|
||||||
getAssessmentItems(this.assessmentId)
|
getAssessmentItems(this.assessmentId),
|
||||||
]).then(([assessmentRes, itemsRes]) => {
|
getAssessmentAnswers(this.assessmentId)
|
||||||
|
]).then(([assessmentRes, itemsRes, answersRes]) => {
|
||||||
const assessment = assessmentRes.data;
|
const assessment = assessmentRes.data;
|
||||||
|
if (!assessment) {
|
||||||
|
this.$modal.msgError("测评不存在");
|
||||||
|
this.$router.push('/psychology/assessment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.scaleName = assessment.scaleName || '未知量表';
|
this.scaleName = assessment.scaleName || '未知量表';
|
||||||
this.itemList = itemsRes.data || [];
|
this.itemList = itemsRes.data || [];
|
||||||
|
|
||||||
|
// 检查题目列表是否为空
|
||||||
|
if (this.itemList.length === 0) {
|
||||||
|
this.$modal.msgWarning("该量表暂无题目,请联系管理员添加题目");
|
||||||
|
this.$router.push('/psychology/assessment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载已保存的答案
|
||||||
|
const savedAnswers = answersRes.data || [];
|
||||||
|
savedAnswers.forEach(answer => {
|
||||||
|
this.answersMap[answer.itemId] = {
|
||||||
|
assessmentId: answer.assessmentId,
|
||||||
|
itemId: answer.itemId,
|
||||||
|
optionId: answer.optionId,
|
||||||
|
optionIds: answer.optionIds,
|
||||||
|
answerScore: answer.answerScore
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 加载所有题目的选项
|
// 加载所有题目的选项
|
||||||
this.loadAllOptions();
|
this.loadAllOptions().then(() => {
|
||||||
|
// 选项加载完成后,加载当前题目的答案
|
||||||
|
this.loadCurrentAnswer();
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载选项失败:', error);
|
||||||
|
this.$modal.msgError("加载题目选项失败,请刷新重试");
|
||||||
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}).catch(() => {
|
}).catch(error => {
|
||||||
|
console.error('加载测评信息失败:', error);
|
||||||
|
this.$modal.msgError("加载测评信息失败,请重试");
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -138,7 +193,7 @@ export default {
|
||||||
this.$set(this.optionMap, item.itemId, response.data || []);
|
this.$set(this.optionMap, item.itemId, response.data || []);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Promise.all(promises);
|
return Promise.all(promises);
|
||||||
},
|
},
|
||||||
/** 答案改变事件 */
|
/** 答案改变事件 */
|
||||||
handleAnswerChange() {
|
handleAnswerChange() {
|
||||||
|
|
@ -158,10 +213,21 @@ export default {
|
||||||
if (selectedOpt) {
|
if (selectedOpt) {
|
||||||
answer.answerScore = selectedOpt.optionScore || 0;
|
answer.answerScore = selectedOpt.optionScore || 0;
|
||||||
}
|
}
|
||||||
this.answersMap[itemId] = answer;
|
// 使用 $set 确保响应式更新
|
||||||
|
this.$set(this.answersMap, itemId, answer);
|
||||||
} else if (this.currentItem.itemType === 'multiple') {
|
} else if (this.currentItem.itemType === 'multiple') {
|
||||||
answer.optionIds = this.selectedOptions.join(',');
|
answer.optionIds = this.selectedOptions.length > 0 ? this.selectedOptions.join(',') : null;
|
||||||
this.answersMap[itemId] = answer;
|
// 计算多选题得分(所有选中选项的分数之和)
|
||||||
|
let totalScore = 0;
|
||||||
|
this.selectedOptions.forEach(optId => {
|
||||||
|
const selectedOpt = this.currentOptions.find(opt => opt.optionId === optId);
|
||||||
|
if (selectedOpt && selectedOpt.optionScore) {
|
||||||
|
totalScore += parseFloat(selectedOpt.optionScore) || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
answer.answerScore = totalScore;
|
||||||
|
// 使用 $set 确保响应式更新
|
||||||
|
this.$set(this.answersMap, itemId, answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动保存答案
|
// 自动保存答案
|
||||||
|
|
@ -170,7 +236,10 @@ export default {
|
||||||
/** 保存答案到服务器 */
|
/** 保存答案到服务器 */
|
||||||
saveAnswerToServer(answer) {
|
saveAnswerToServer(answer) {
|
||||||
saveAnswer(answer).then(() => {
|
saveAnswer(answer).then(() => {
|
||||||
// 静默保存
|
// 静默保存成功
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('保存答案失败:', error);
|
||||||
|
// 不显示错误提示,避免打断用户答题
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** 上一题 */
|
/** 上一题 */
|
||||||
|
|
@ -189,15 +258,26 @@ export default {
|
||||||
},
|
},
|
||||||
/** 加载当前题目的答案 */
|
/** 加载当前题目的答案 */
|
||||||
loadCurrentAnswer() {
|
loadCurrentAnswer() {
|
||||||
|
if (!this.currentItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const itemId = this.currentItem.itemId;
|
const itemId = this.currentItem.itemId;
|
||||||
const answer = this.answersMap[itemId];
|
const answer = this.answersMap[itemId];
|
||||||
|
|
||||||
if (this.currentItem.itemType === 'single') {
|
if (this.currentItem.itemType === 'single') {
|
||||||
this.selectedOption = answer ? answer.optionId : null;
|
this.selectedOption = answer && answer.optionId ? answer.optionId : null;
|
||||||
this.selectedOptions = [];
|
this.selectedOptions = [];
|
||||||
} else if (this.currentItem.itemType === 'multiple') {
|
} else if (this.currentItem.itemType === 'multiple') {
|
||||||
this.selectedOption = null;
|
this.selectedOption = null;
|
||||||
this.selectedOptions = answer ? answer.optionIds.split(',') : [];
|
if (answer && answer.optionIds) {
|
||||||
|
// 将字符串转换为数字数组
|
||||||
|
this.selectedOptions = answer.optionIds.split(',').map(id => parseInt(id)).filter(id => !isNaN(id));
|
||||||
|
} else {
|
||||||
|
this.selectedOptions = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedOption = null;
|
||||||
|
this.selectedOptions = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 暂停测评 */
|
/** 暂停测评 */
|
||||||
|
|
@ -217,9 +297,20 @@ export default {
|
||||||
},
|
},
|
||||||
/** 提交测评 */
|
/** 提交测评 */
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
|
// 检查是否所有题目都已作答
|
||||||
|
if (!this.isComplete) {
|
||||||
|
const remaining = this.itemList.length - this.answeredCount;
|
||||||
|
this.$modal.msgWarning(`还有 ${remaining} 道题目未作答,请完成所有题目后再提交`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$modal.confirm('确定要提交测评吗?提交后将不能修改。').then(() => {
|
this.$modal.confirm('确定要提交测评吗?提交后将不能修改。').then(() => {
|
||||||
this.$modal.msgSuccess("测评已提交");
|
submitAssessment(this.assessmentId).then(response => {
|
||||||
this.$router.push('/psychology/assessment');
|
this.$modal.msgSuccess(response.msg || "测评已提交,报告已生成");
|
||||||
|
this.$router.push('/psychology/assessment');
|
||||||
|
}).catch(error => {
|
||||||
|
this.$modal.msgError(error.msg || "提交失败,请重试");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -58,14 +58,26 @@ export default {
|
||||||
const reportId = this.$route.query.reportId;
|
const reportId = this.$route.query.reportId;
|
||||||
const assessmentId = this.$route.query.assessmentId;
|
const assessmentId = this.$route.query.assessmentId;
|
||||||
|
|
||||||
|
if (!reportId && !assessmentId) {
|
||||||
|
this.loading = false;
|
||||||
|
this.$modal.msgError("缺少报告ID或测评ID参数");
|
||||||
|
this.$router.push('/psychology/report');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadFunc = reportId ? getReport(reportId) : getReportByAssessmentId(assessmentId);
|
const loadFunc = reportId ? getReport(reportId) : getReportByAssessmentId(assessmentId);
|
||||||
|
|
||||||
loadFunc.then(response => {
|
loadFunc.then(response => {
|
||||||
this.reportForm = response.data || {};
|
if (response.data) {
|
||||||
|
this.reportForm = response.data;
|
||||||
|
} else {
|
||||||
|
this.$modal.msgWarning("报告不存在");
|
||||||
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}).catch(() => {
|
}).catch(error => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.$modal.msgError("加载报告失败");
|
console.error('加载报告失败:', error);
|
||||||
|
this.$modal.msgError("加载报告失败,请检查报告是否存在");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** 返回 */
|
/** 返回 */
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ export default {
|
||||||
},
|
},
|
||||||
/** 查看按钮操作 */
|
/** 查看按钮操作 */
|
||||||
handleView(row) {
|
handleView(row) {
|
||||||
this.$router.push({ path: '/psychology/report/detail', query: { reportId: row.reportId } });
|
this.$router.push({ path: 'report/detail', query: { reportId: row.reportId } });
|
||||||
},
|
},
|
||||||
/** 修改按钮操作 */
|
/** 修改按钮操作 */
|
||||||
handleUpdate(row) {
|
handleUpdate(row) {
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,16 @@ public class PsyAssessmentController extends BaseController
|
||||||
return success(items);
|
return success(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取测评的答案列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/answers/{assessmentId}")
|
||||||
|
public AjaxResult getAnswers(@PathVariable Long assessmentId)
|
||||||
|
{
|
||||||
|
List<PsyAssessmentAnswer> answers = answerService.selectAnswerListByAssessmentId(assessmentId);
|
||||||
|
return success(answers);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存答案
|
* 保存答案
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# 项目相关配置
|
# 项目相关配置
|
||||||
ruoyi:
|
ruoyi:
|
||||||
# 名称
|
# 名称
|
||||||
name: 动动脑新闻系统
|
name: 动动脑新闻系统
|
||||||
|
|
@ -77,7 +77,8 @@ spring:
|
||||||
# password: xbZttkmndxCkWsycjs2
|
# password: xbZttkmndxCkWsycjs2
|
||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
lettuce:
|
# 使用Jedis客户端替代Lettuce客户端
|
||||||
|
jedis:
|
||||||
pool:
|
pool:
|
||||||
# 连接池中的最小空闲连接
|
# 连接池中的最小空闲连接
|
||||||
min-idle: 0
|
min-idle: 0
|
||||||
|
|
@ -85,7 +86,7 @@ spring:
|
||||||
max-idle: 8
|
max-idle: 8
|
||||||
# 连接池的最大数据库连接数
|
# 连接池的最大数据库连接数
|
||||||
max-active: 8
|
max-active: 8
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
# 连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||||
max-wait: -1ms
|
max-wait: -1ms
|
||||||
|
|
||||||
# token配置
|
# token配置
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
@ -99,6 +99,19 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<!-- 排除默认的Lettuce客户端 -->
|
||||||
|
<exclusion>
|
||||||
|
<groupId>io.lettuce</groupId>
|
||||||
|
<artifactId>lettuce-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jedis客户端 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>redis.clients</groupId>
|
||||||
|
<artifactId>jedis</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- pool 对象池 -->
|
<!-- pool 对象池 -->
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package com.ddnai.common.core.domain.model;
|
package com.ddnai.common.core.domain.model;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.annotation.JSONField;
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
import com.ddnai.common.core.domain.entity.SysUser;
|
import com.ddnai.common.core.domain.entity.SysUser;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
@ -12,66 +14,82 @@ import java.util.Set;
|
||||||
*
|
*
|
||||||
* @author ddnai
|
* @author ddnai
|
||||||
*/
|
*/
|
||||||
public class LoginUser implements UserDetails
|
public class LoginUser implements UserDetails, Serializable, Cloneable
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户ID
|
* 用户ID
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "userId", ordinal = 1)
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 部门ID
|
* 部门ID
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "deptId", ordinal = 2)
|
||||||
private Long deptId;
|
private Long deptId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户唯一标识
|
* 用户唯一标识
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "token", ordinal = 3)
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录时间
|
* 登录时间
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "loginTime", ordinal = 4)
|
||||||
private Long loginTime;
|
private Long loginTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 过期时间
|
* 过期时间
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "expireTime", ordinal = 5)
|
||||||
private Long expireTime;
|
private Long expireTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录IP地址
|
* 登录IP地址
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "ipaddr", ordinal = 6)
|
||||||
private String ipaddr;
|
private String ipaddr;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录地点
|
* 登录地点
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "loginLocation", ordinal = 7)
|
||||||
private String loginLocation;
|
private String loginLocation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器类型
|
* 浏览器类型
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "browser", ordinal = 8)
|
||||||
private String browser;
|
private String browser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 操作系统
|
* 操作系统
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "os", ordinal = 9)
|
||||||
private String os;
|
private String os;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限列表
|
* 权限列表
|
||||||
*/
|
*/
|
||||||
|
@JSONField(name = "permissions", ordinal = 10)
|
||||||
private Set<String> permissions;
|
private Set<String> permissions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户信息
|
* 用户信息
|
||||||
*/
|
*/
|
||||||
@JSONField(name = "user")
|
@JSONField(name = "user", ordinal = 11, serialize = true, deserialize = true)
|
||||||
private SysUser user;
|
private SysUser user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
@JSONField(name = "username", ordinal = 12)
|
||||||
|
private String username;
|
||||||
|
|
||||||
public LoginUser()
|
public LoginUser()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -80,6 +98,10 @@ public class LoginUser implements UserDetails
|
||||||
{
|
{
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
|
// 同步用户名
|
||||||
|
if (user != null && user.getUserName() != null) {
|
||||||
|
this.username = user.getUserName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
|
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
|
||||||
|
|
@ -88,6 +110,10 @@ public class LoginUser implements UserDetails
|
||||||
this.deptId = deptId;
|
this.deptId = deptId;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.permissions = permissions;
|
this.permissions = permissions;
|
||||||
|
// 同步用户名
|
||||||
|
if (user != null && user.getUserName() != null) {
|
||||||
|
this.username = user.getUserName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getUserId()
|
public Long getUserId()
|
||||||
|
|
@ -120,6 +146,20 @@ public class LoginUser implements UserDetails
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername()
|
||||||
|
{
|
||||||
|
if (this.username != null) {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
return user != null ? user.getUserName() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username)
|
||||||
|
{
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
@JSONField(serialize = false)
|
@JSONField(serialize = false)
|
||||||
@Override
|
@Override
|
||||||
public String getPassword()
|
public String getPassword()
|
||||||
|
|
@ -127,12 +167,6 @@ public class LoginUser implements UserDetails
|
||||||
return user != null ? user.getPassword() : null;
|
return user != null ? user.getPassword() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUsername()
|
|
||||||
{
|
|
||||||
return user != null ? user.getUserName() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 账户是否未过期,过期无法验证
|
* 账户是否未过期,过期无法验证
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,7 +210,7 @@ public class LoginUser implements UserDetails
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled()
|
public boolean isEnabled()
|
||||||
{
|
{
|
||||||
return true;
|
return user != null && "0".equals(user.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getLoginTime()
|
public Long getLoginTime()
|
||||||
|
|
@ -257,6 +291,10 @@ public class LoginUser implements UserDetails
|
||||||
public void setUser(SysUser user)
|
public void setUser(SysUser user)
|
||||||
{
|
{
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
// 同步用户名
|
||||||
|
if (user != null && user.getUserName() != null) {
|
||||||
|
this.username = user.getUserName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -264,5 +302,31 @@ public class LoginUser implements UserDetails
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加克隆方法,避免序列化时的引用问题
|
||||||
|
@Override
|
||||||
|
public Object clone() throws CloneNotSupportedException {
|
||||||
|
return super.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加toString方法,便于调试
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return JSON.toJSONString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加equals和hashCode方法,确保对象比较正确
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) return true;
|
||||||
|
if (obj == null || getClass() != obj.getClass()) return false;
|
||||||
|
LoginUser loginUser = (LoginUser) obj;
|
||||||
|
return userId != null ? userId.equals(loginUser.userId) : loginUser.userId == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return userId != null ? userId.hashCode() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
package com.ddnai.framework.config;
|
package com.ddnai.framework.config;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONFactory;
|
||||||
|
import com.alibaba.fastjson2.JSONWriter;
|
||||||
|
import com.alibaba.fastjson2.reader.ObjectReaderProvider;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||||
import org.springframework.data.redis.serializer.SerializationException;
|
import org.springframework.data.redis.serializer.SerializationException;
|
||||||
import com.alibaba.fastjson2.JSON;
|
|
||||||
import com.alibaba.fastjson2.JSONReader;
|
import com.ddnai.common.core.domain.model.LoginUser;
|
||||||
import com.alibaba.fastjson2.JSONWriter;
|
|
||||||
import com.alibaba.fastjson2.filter.Filter;
|
|
||||||
import com.ddnai.common.constant.Constants;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis使用FastJson序列化
|
* Redis使用FastJson序列化
|
||||||
*
|
*
|
||||||
* @author ddnai
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||||
{
|
{
|
||||||
|
|
@ -22,10 +24,21 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||||
|
|
||||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||||
|
|
||||||
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
|
|
||||||
|
|
||||||
private Class<T> clazz;
|
private Class<T> clazz;
|
||||||
|
|
||||||
|
static
|
||||||
|
{
|
||||||
|
// 配置ObjectReaderProvider(FastJSON2 2.x 中替代ParserConfig)
|
||||||
|
ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
|
||||||
|
|
||||||
|
// 添加包到自动类型白名单
|
||||||
|
provider.addAutoTypeAccept("com.ddnai");
|
||||||
|
provider.addAutoTypeAccept("java.util");
|
||||||
|
provider.addAutoTypeAccept("java.lang");
|
||||||
|
|
||||||
|
log.info("FastJson ObjectReaderProvider 初始化完成,已添加关键包到白名单");
|
||||||
|
}
|
||||||
|
|
||||||
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
|
|
@ -39,12 +52,11 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||||
{
|
{
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
try {
|
||||||
|
// 使用基本的序列化配置
|
||||||
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
||||||
}
|
} catch (Exception e) {
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
log.error("序列化对象失败: {}", e.getMessage(), e);
|
log.error("序列化对象失败: {}", e.getMessage(), e);
|
||||||
throw new SerializationException("序列化对象失败", e);
|
throw new SerializationException("序列化对象失败", e);
|
||||||
}
|
}
|
||||||
|
|
@ -57,23 +69,60 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
String str = null;
|
||||||
String str = new String(bytes, DEFAULT_CHARSET);
|
try {
|
||||||
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
|
str = new String(bytes, DEFAULT_CHARSET);
|
||||||
}
|
|
||||||
catch (Exception e)
|
// 对于LoginUser类型,使用更安全的反序列化方式
|
||||||
{
|
if (isLoginUserClass(clazz) || str.contains("LoginUser")) {
|
||||||
log.error("反序列化对象失败,类型: {}, 错误: {}", clazz.getName(), e.getMessage());
|
log.debug("处理LoginUser类型的反序列化");
|
||||||
// 记录前200个字符的JSON内容,便于调试
|
try {
|
||||||
if (bytes.length > 0)
|
return JSON.parseObject(str, clazz);
|
||||||
{
|
} catch (ClassCastException e) {
|
||||||
String preview = new String(bytes, 0, Math.min(200, bytes.length), DEFAULT_CHARSET);
|
// 如果出现类型转换异常,抛出SerializationException以便上层处理
|
||||||
log.error("JSON内容预览: {}", preview);
|
log.warn("反序列化LoginUser时出现类型转换异常,可能是Redis缓存数据损坏: {}", e.getMessage());
|
||||||
|
throw new SerializationException("反序列化LoginUser失败,缓存数据可能损坏", e);
|
||||||
|
} catch (com.alibaba.fastjson2.JSONException e) {
|
||||||
|
// FastJSON2的JSONException,也转换为SerializationException
|
||||||
|
log.warn("反序列化LoginUser时出现JSON异常: {}", e.getMessage());
|
||||||
|
throw new SerializationException("反序列化LoginUser失败,JSON格式错误", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 不抛出异常,返回null,让调用方处理
|
|
||||||
return null;
|
// 其他类型使用标准配置
|
||||||
|
return JSON.parseObject(str, clazz);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
// 处理类型转换异常,抛出SerializationException以便上层处理
|
||||||
|
log.error("反序列化对象时出现类型转换异常,类型: {}, 错误: {}",
|
||||||
|
clazz != null ? clazz.getName() : "null",
|
||||||
|
e.getMessage());
|
||||||
|
throw new SerializationException("反序列化对象失败,类型转换异常", e);
|
||||||
|
} catch (com.alibaba.fastjson2.JSONException e) {
|
||||||
|
// FastJSON2的JSONException,转换为SerializationException
|
||||||
|
String jsonPreview = (str != null && str.length() > 200) ? str.substring(0, 200) + "..." : (str != null ? str : "null");
|
||||||
|
log.error("反序列化对象失败(JSON异常),类型: {}, JSON内容预览: {}",
|
||||||
|
clazz != null ? clazz.getName() : "null",
|
||||||
|
jsonPreview);
|
||||||
|
throw new SerializationException("反序列化对象失败,JSON格式错误", e);
|
||||||
|
} catch (SerializationException e) {
|
||||||
|
// 重新抛出SerializationException
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
String jsonPreview = (str != null && str.length() > 200) ? str.substring(0, 200) + "..." : (str != null ? str : "null");
|
||||||
|
log.error("反序列化对象失败,类型: {}, JSON内容预览: {}",
|
||||||
|
clazz != null ? clazz.getName() : "null",
|
||||||
|
jsonPreview, e);
|
||||||
|
// 转换为SerializationException
|
||||||
|
throw new SerializationException("反序列化对象失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否是LoginUser相关类
|
||||||
|
private boolean isLoginUserClass(Class<?> cls) {
|
||||||
|
return cls != null && (cls == LoginUser.class ||
|
||||||
|
cls.getName().contains("LoginUser") ||
|
||||||
|
cls == Object.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,23 @@ public class RedisConfig extends CachingConfigurerSupport
|
||||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||||
{
|
{
|
||||||
|
// 创建RedisTemplate实例
|
||||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||||
|
// 设置连接工厂
|
||||||
template.setConnectionFactory(connectionFactory);
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
// 使用StringRedisSerializer作为key的序列化器
|
||||||
|
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
template.setKeySerializer(stringRedisSerializer);
|
||||||
template.setKeySerializer(new StringRedisSerializer());
|
template.setHashKeySerializer(stringRedisSerializer);
|
||||||
template.setValueSerializer(serializer);
|
|
||||||
|
// 使用FastJson2JsonRedisSerializer作为value的序列化器
|
||||||
// Hash的key也采用StringRedisSerializer的序列化方式
|
// 这个序列化器已经配置了安全的类型处理和白名单
|
||||||
template.setHashKeySerializer(new StringRedisSerializer());
|
FastJson2JsonRedisSerializer<Object> fastJsonSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
|
||||||
template.setHashValueSerializer(serializer);
|
template.setValueSerializer(fastJsonSerializer);
|
||||||
|
template.setHashValueSerializer(fastJsonSerializer);
|
||||||
|
|
||||||
|
// 初始化RedisTemplate
|
||||||
template.afterPropertiesSet();
|
template.afterPropertiesSet();
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,26 +65,67 @@ public class TokenService
|
||||||
String token = getToken(request);
|
String token = getToken(request);
|
||||||
if (StringUtils.isNotEmpty(token))
|
if (StringUtils.isNotEmpty(token))
|
||||||
{
|
{
|
||||||
|
String uuid = null;
|
||||||
|
String userKey = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Claims claims = parseToken(token);
|
Claims claims = parseToken(token);
|
||||||
// 解析对应的权限以及用户信息
|
// 解析对应的权限以及用户信息
|
||||||
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||||
String userKey = getTokenKey(uuid);
|
if (StringUtils.isEmpty(uuid))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
userKey = getTokenKey(uuid);
|
||||||
LoginUser user = redisCache.getCacheObject(userKey);
|
LoginUser user = redisCache.getCacheObject(userKey);
|
||||||
// 验证用户对象是否完整
|
// 验证用户对象是否完整
|
||||||
if (user != null && user.getUser() == null)
|
if (user != null && user.getUser() == null)
|
||||||
{
|
{
|
||||||
log.warn("用户缓存数据不完整,token: {}", uuid);
|
log.warn("用户缓存数据不完整,token: {},删除损坏的缓存", uuid);
|
||||||
// 删除损坏的缓存
|
// 删除损坏的缓存
|
||||||
redisCache.deleteObject(userKey);
|
redisCache.deleteObject(userKey);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
catch (org.springframework.data.redis.serializer.SerializationException e)
|
||||||
|
{
|
||||||
|
// 反序列化异常,通常是Redis缓存数据损坏
|
||||||
|
log.error("反序列化用户缓存失败,token: {},清理损坏的缓存: {}", uuid, e.getMessage());
|
||||||
|
if (StringUtils.isNotEmpty(uuid) && StringUtils.isNotEmpty(userKey))
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
redisCache.deleteObject(userKey);
|
||||||
|
log.info("已清理损坏的用户缓存,token: {}", uuid);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.debug("清理缓存时出现异常: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
log.error("获取用户信息异常'{}'", e.getMessage());
|
log.error("获取用户信息异常'{}',尝试清理可能损坏的缓存", e.getMessage());
|
||||||
|
// 如果出现异常,尝试清理可能损坏的缓存
|
||||||
|
if (StringUtils.isEmpty(uuid))
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.debug("解析token时出现异常: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(uuid))
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
userKey = getTokenKey(uuid);
|
||||||
|
redisCache.deleteObject(userKey);
|
||||||
|
log.info("已清理损坏的用户缓存,token: {}", uuid);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.debug("清理缓存时出现异常: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ public class PsyAssessment extends BaseEntity
|
||||||
/** 量表ID */
|
/** 量表ID */
|
||||||
private Long scaleId;
|
private Long scaleId;
|
||||||
|
|
||||||
|
/** 量表名称(关联查询字段,不存储在表中) */
|
||||||
|
private String scaleName;
|
||||||
|
|
||||||
/** 用户ID */
|
/** 用户ID */
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
|
|
@ -96,6 +99,16 @@ public class PsyAssessment extends BaseEntity
|
||||||
this.scaleId = scaleId;
|
this.scaleId = scaleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getScaleName()
|
||||||
|
{
|
||||||
|
return scaleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScaleName(String scaleName)
|
||||||
|
{
|
||||||
|
this.scaleName = scaleName;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getUserId()
|
public Long getUserId()
|
||||||
{
|
{
|
||||||
return userId;
|
return userId;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.ddnai.system.mapper.psychology;
|
package com.ddnai.system.mapper.psychology;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
|
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,7 +27,7 @@ public interface PsyResultInterpretationMapper
|
||||||
* @param score 得分
|
* @param score 得分
|
||||||
* @return 结果解释信息
|
* @return 结果解释信息
|
||||||
*/
|
*/
|
||||||
public PsyResultInterpretation selectInterpretationByScore(Long scaleId, Long factorId, java.math.BigDecimal score);
|
public PsyResultInterpretation selectInterpretationByScore(@Param("scaleId") Long scaleId, @Param("factorId") Long factorId, @Param("score") java.math.BigDecimal score);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询结果解释列表
|
* 查询结果解释列表
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.ddnai.system.mapper.psychology;
|
package com.ddnai.system.mapper.psychology;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ddnai.system.domain.psychology.PsyWarningRule;
|
import com.ddnai.system.domain.psychology.PsyWarningRule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,7 +42,7 @@ public interface PsyWarningRuleMapper
|
||||||
* @param factorId 因子ID(为空表示总分配置)
|
* @param factorId 因子ID(为空表示总分配置)
|
||||||
* @return 预警规则集合
|
* @return 预警规则集合
|
||||||
*/
|
*/
|
||||||
public List<PsyWarningRule> selectEnabledWarningRuleList(Long scaleId, Long factorId);
|
public List<PsyWarningRule> selectEnabledWarningRuleList(@Param("scaleId") Long scaleId, @Param("factorId") Long factorId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增预警规则
|
* 新增预警规则
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
(#{item.assessmentId}, #{item.itemId}, #{item.optionId}, #{item.optionIds}, #{item.answerScore}, #{item.answerText}, sysdate())
|
(#{item.assessmentId}, #{item.itemId}, #{item.optionId}, #{item.optionIds}, #{item.answerScore}, #{item.answerText}, sysdate())
|
||||||
</foreach>
|
</foreach>
|
||||||
on duplicate key update
|
on duplicate key update
|
||||||
option_id = values(option_id),
|
option_id = VALUES(option_id),
|
||||||
option_ids = values(option_ids),
|
option_ids = VALUES(option_ids),
|
||||||
answer_score = values(answer_score),
|
answer_score = VALUES(answer_score),
|
||||||
answer_text = values(answer_text),
|
answer_text = VALUES(answer_text),
|
||||||
create_time = sysdate()
|
create_time = sysdate()
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<resultMap type="com.ddnai.system.domain.psychology.PsyAssessment" id="PsyAssessmentResult">
|
<resultMap type="com.ddnai.system.domain.psychology.PsyAssessment" id="PsyAssessmentResult">
|
||||||
<result property="assessmentId" column="assessment_id" />
|
<result property="assessmentId" column="assessment_id" />
|
||||||
<result property="scaleId" column="scale_id" />
|
<result property="scaleId" column="scale_id" />
|
||||||
|
<result property="scaleName" column="scale_name" />
|
||||||
<result property="userId" column="user_id" />
|
<result property="userId" column="user_id" />
|
||||||
<result property="assesseeName" column="assessee_name" />
|
<result property="assesseeName" column="assessee_name" />
|
||||||
<result property="assesseeGender" column="assessee_gender" />
|
<result property="assesseeGender" column="assessee_gender" />
|
||||||
|
|
@ -32,47 +33,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectAssessmentVo">
|
<sql id="selectAssessmentVo">
|
||||||
select assessment_id, scale_id, user_id, assessee_name, assessee_gender, assessee_age,
|
select a.assessment_id, a.scale_id, s.scale_name, a.user_id, a.assessee_name, a.assessee_gender, a.assessee_age,
|
||||||
assessee_id_card, assessee_phone, assessee_email, start_time, pause_time,
|
a.assessee_id_card, a.assessee_phone, a.assessee_email, a.start_time, a.pause_time,
|
||||||
resume_time, pause_count, submit_time, complete_time, total_score, status,
|
a.resume_time, a.pause_count, a.submit_time, a.complete_time, a.total_score, a.status,
|
||||||
ip_address, user_agent, create_by, create_time, update_by, update_time, remark
|
a.ip_address, a.user_agent, a.create_by, a.create_time, a.update_by, a.update_time, a.remark
|
||||||
from psy_assessment
|
from psy_assessment a
|
||||||
|
left join psy_scale s on a.scale_id = s.scale_id
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectAssessmentById" parameterType="Long" resultMap="PsyAssessmentResult">
|
<select id="selectAssessmentById" parameterType="Long" resultMap="PsyAssessmentResult">
|
||||||
<include refid="selectAssessmentVo"/>
|
<include refid="selectAssessmentVo"/>
|
||||||
where assessment_id = #{assessmentId}
|
where a.assessment_id = #{assessmentId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectAssessmentList" parameterType="com.ddnai.system.domain.psychology.PsyAssessment" resultMap="PsyAssessmentResult">
|
<select id="selectAssessmentList" parameterType="com.ddnai.system.domain.psychology.PsyAssessment" resultMap="PsyAssessmentResult">
|
||||||
<include refid="selectAssessmentVo"/>
|
<include refid="selectAssessmentVo"/>
|
||||||
<where>
|
<where>
|
||||||
<if test="scaleId != null">
|
<if test="scaleId != null">
|
||||||
AND scale_id = #{scaleId}
|
AND a.scale_id = #{scaleId}
|
||||||
</if>
|
</if>
|
||||||
<if test="userId != null">
|
<if test="userId != null">
|
||||||
AND user_id = #{userId}
|
AND a.user_id = #{userId}
|
||||||
</if>
|
</if>
|
||||||
<if test="assesseeName != null and assesseeName != ''">
|
<if test="assesseeName != null and assesseeName != ''">
|
||||||
AND assessee_name like concat('%', #{assesseeName}, '%')
|
AND a.assessee_name like concat('%', #{assesseeName}, '%')
|
||||||
</if>
|
</if>
|
||||||
<if test="status != null and status != ''">
|
<if test="status != null and status != ''">
|
||||||
AND status = #{status}
|
AND a.status = #{status}
|
||||||
</if>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
order by create_time desc
|
order by a.create_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectAssessmentListByUserId" parameterType="Long" resultMap="PsyAssessmentResult">
|
<select id="selectAssessmentListByUserId" parameterType="Long" resultMap="PsyAssessmentResult">
|
||||||
<include refid="selectAssessmentVo"/>
|
<include refid="selectAssessmentVo"/>
|
||||||
where user_id = #{userId}
|
where a.user_id = #{userId}
|
||||||
order by create_time desc
|
order by a.create_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectPausedAssessmentList" parameterType="Long" resultMap="PsyAssessmentResult">
|
<select id="selectPausedAssessmentList" parameterType="Long" resultMap="PsyAssessmentResult">
|
||||||
<include refid="selectAssessmentVo"/>
|
<include refid="selectAssessmentVo"/>
|
||||||
where user_id = #{userId} and status = '3'
|
where a.user_id = #{userId} and a.status = '3'
|
||||||
order by pause_time desc
|
order by a.pause_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertAssessment" parameterType="com.ddnai.system.domain.psychology.PsyAssessment" useGeneratedKeys="true" keyProperty="assessmentId">
|
<insert id="insertAssessment" parameterType="com.ddnai.system.domain.psychology.PsyAssessment" useGeneratedKeys="true" keyProperty="assessmentId">
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateBy" column="update_by" />
|
<result property="updateBy" column="update_by" />
|
||||||
<result property="updateTime" column="update_time" />
|
<result property="updateTime" column="update_time" />
|
||||||
<result property="remark" column="remark" />
|
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectReportVo">
|
<sql id="selectReportVo">
|
||||||
select report_id, assessment_id, report_type, report_title, report_content, summary,
|
select report_id, assessment_id, report_type, report_title, report_content, summary,
|
||||||
chart_data, pdf_path, is_generated, generate_time, create_by, create_time,
|
chart_data, pdf_path, is_generated, generate_time, create_by, create_time,
|
||||||
update_by, update_time, remark
|
update_by, update_time
|
||||||
from psy_assessment_report
|
from psy_assessment_report
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
|
|
@ -66,7 +65,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<if test="pdfPath != null and pdfPath != ''">pdf_path, </if>
|
<if test="pdfPath != null and pdfPath != ''">pdf_path, </if>
|
||||||
<if test="isGenerated != null and isGenerated != ''">is_generated, </if>
|
<if test="isGenerated != null and isGenerated != ''">is_generated, </if>
|
||||||
<if test="generateTime != null">generate_time, </if>
|
<if test="generateTime != null">generate_time, </if>
|
||||||
<if test="remark != null">remark,</if>
|
|
||||||
<if test="createBy != null and createBy != ''">create_by,</if>
|
<if test="createBy != null and createBy != ''">create_by,</if>
|
||||||
create_time
|
create_time
|
||||||
)values(
|
)values(
|
||||||
|
|
@ -79,7 +77,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<if test="pdfPath != null and pdfPath != ''">#{pdfPath}, </if>
|
<if test="pdfPath != null and pdfPath != ''">#{pdfPath}, </if>
|
||||||
<if test="isGenerated != null and isGenerated != ''">#{isGenerated}, </if>
|
<if test="isGenerated != null and isGenerated != ''">#{isGenerated}, </if>
|
||||||
<if test="generateTime != null">#{generateTime}, </if>
|
<if test="generateTime != null">#{generateTime}, </if>
|
||||||
<if test="remark != null">#{remark},</if>
|
|
||||||
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
<if test="createBy != null and createBy != ''">#{createBy},</if>
|
||||||
sysdate()
|
sysdate()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,30 +28,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectProfileVo">
|
<sql id="selectProfileVo">
|
||||||
select profile_id, user_id, profile_type, profile_data, avatar, id_card, user_name, phone, birthday,
|
select p.profile_id, p.user_id, p.profile_type, p.profile_data, p.avatar, p.id_card, p.birthday,
|
||||||
education, occupation, address, emergency_contact, emergency_phone,
|
p.education, p.occupation, p.address, p.emergency_contact, p.emergency_phone,
|
||||||
medical_history, create_by, create_time, update_by, update_time, remark
|
p.medical_history, p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
|
||||||
from psy_user_profile
|
u.user_name, u.phonenumber as phone
|
||||||
|
from psy_user_profile p
|
||||||
|
left join sys_user u on p.user_id = u.user_id
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectProfileById" parameterType="Long" resultMap="PsyUserProfileResult">
|
<select id="selectProfileById" parameterType="Long" resultMap="PsyUserProfileResult">
|
||||||
<include refid="selectProfileVo"/>
|
<include refid="selectProfileVo"/>
|
||||||
where profile_id = #{profileId}
|
where p.profile_id = #{profileId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectProfileByUserId" parameterType="Long" resultMap="PsyUserProfileResult">
|
<select id="selectProfileByUserId" parameterType="Long" resultMap="PsyUserProfileResult">
|
||||||
<include refid="selectProfileVo"/>
|
<include refid="selectProfileVo"/>
|
||||||
where user_id = #{userId}
|
where p.user_id = #{userId}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectProfileList" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" resultMap="PsyUserProfileResult">
|
<select id="selectProfileList" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" resultMap="PsyUserProfileResult">
|
||||||
<include refid="selectProfileVo"/>
|
<include refid="selectProfileVo"/>
|
||||||
<where>
|
<where>
|
||||||
<if test="userId != null"> and user_id = #{userId}</if>
|
<if test="userId != null"> and p.user_id = #{userId}</if>
|
||||||
<if test="profileType != null and profileType != ''"> and profile_type = #{profileType}</if>
|
<if test="profileType != null and profileType != ''"> and p.profile_type = #{profileType}</if>
|
||||||
<if test="idCard != null and idCard != ''"> and id_card = #{idCard}</if>
|
<if test="idCard != null and idCard != ''"> and p.id_card = #{idCard}</if>
|
||||||
</where>
|
</where>
|
||||||
order by create_time desc
|
order by p.create_time desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertProfile" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" useGeneratedKeys="true" keyProperty="profileId">
|
<insert id="insertProfile" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" useGeneratedKeys="true" keyProperty="profileId">
|
||||||
|
|
@ -61,8 +63,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<if test="profileData != null">profile_data, </if>
|
<if test="profileData != null">profile_data, </if>
|
||||||
<if test="avatar != null">avatar, </if>
|
<if test="avatar != null">avatar, </if>
|
||||||
<if test="idCard != null">id_card, </if>
|
<if test="idCard != null">id_card, </if>
|
||||||
<if test="userName != null">user_name, </if>
|
|
||||||
<if test="phone != null">phone, </if>
|
|
||||||
<if test="birthday != null">birthday, </if>
|
<if test="birthday != null">birthday, </if>
|
||||||
<if test="education != null">education, </if>
|
<if test="education != null">education, </if>
|
||||||
<if test="occupation != null">occupation, </if>
|
<if test="occupation != null">occupation, </if>
|
||||||
|
|
@ -79,8 +79,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<if test="profileData != null">#{profileData}, </if>
|
<if test="profileData != null">#{profileData}, </if>
|
||||||
<if test="avatar != null">#{avatar}, </if>
|
<if test="avatar != null">#{avatar}, </if>
|
||||||
<if test="idCard != null">#{idCard}, </if>
|
<if test="idCard != null">#{idCard}, </if>
|
||||||
<if test="userName != null">#{userName}, </if>
|
|
||||||
<if test="phone != null">#{phone}, </if>
|
|
||||||
<if test="birthday != null">#{birthday}, </if>
|
<if test="birthday != null">#{birthday}, </if>
|
||||||
<if test="education != null">#{education}, </if>
|
<if test="education != null">#{education}, </if>
|
||||||
<if test="occupation != null">#{occupation}, </if>
|
<if test="occupation != null">#{occupation}, </if>
|
||||||
|
|
@ -101,8 +99,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
<if test="profileData != null">profile_data = #{profileData}, </if>
|
<if test="profileData != null">profile_data = #{profileData}, </if>
|
||||||
<if test="avatar != null">avatar = #{avatar}, </if>
|
<if test="avatar != null">avatar = #{avatar}, </if>
|
||||||
<if test="idCard != null">id_card = #{idCard}, </if>
|
<if test="idCard != null">id_card = #{idCard}, </if>
|
||||||
<if test="userName != null">user_name = #{userName}, </if>
|
|
||||||
<if test="phone != null">phone = #{phone}, </if>
|
|
||||||
<if test="birthday != null">birthday = #{birthday}, </if>
|
<if test="birthday != null">birthday = #{birthday}, </if>
|
||||||
<if test="education != null">education = #{education}, </if>
|
<if test="education != null">education = #{education}, </if>
|
||||||
<if test="occupation != null">occupation = #{occupation}, </if>
|
<if test="occupation != null">occupation = #{occupation}, </if>
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# 菜单重复问题解决方案
|
|
||||||
|
|
||||||
## 🔍 问题说明
|
|
||||||
|
|
||||||
如果浏览器中出现大量重复的菜单,可能是因为:
|
|
||||||
1. SQL脚本多次执行导致重复插入
|
|
||||||
2. 数据库中存在重复的菜单记录
|
|
||||||
|
|
||||||
## 📋 解决步骤
|
|
||||||
|
|
||||||
### 第一步:检查重复菜单
|
|
||||||
|
|
||||||
执行检查脚本,查看数据库中的重复菜单情况:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -u root -p ry_news < sql/check_duplicate_menus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
或在MySQL客户端中执行:
|
|
||||||
```sql
|
|
||||||
source sql/check_duplicate_menus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二步:清理重复菜单
|
|
||||||
|
|
||||||
确认有重复菜单后,执行清理脚本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -u root -p ry_news < sql/cleanup_duplicate_menus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
或在MySQL客户端中执行:
|
|
||||||
```sql
|
|
||||||
source sql/cleanup_duplicate_menus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第三步:验证清理结果
|
|
||||||
|
|
||||||
执行检查脚本再次验证,确认没有重复菜单:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
source sql/check_duplicate_menus.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
1. **备份数据库**:在执行清理脚本前,请先备份数据库
|
|
||||||
2. **检查结果**:清理脚本会保留menu_id最小的菜单,删除其他重复项
|
|
||||||
3. **重新登录**:清理后,需要重新登录系统才能看到更新后的菜单
|
|
||||||
|
|
||||||
## 📁 SQL文件说明
|
|
||||||
|
|
||||||
- `check_duplicate_menus.sql` - 检查重复菜单的查询脚本(只读,不会修改数据)
|
|
||||||
- `cleanup_duplicate_menus.sql` - 清理重复菜单的执行脚本(会删除重复数据)
|
|
||||||
- `psychological_system_complete.sql` - 主SQL文件(包含所有表结构和初始数据)
|
|
||||||
|
|
||||||
## 🔧 如果问题仍然存在
|
|
||||||
|
|
||||||
如果清理后仍然出现重复菜单,请检查:
|
|
||||||
|
|
||||||
1. **前端缓存**:清除浏览器缓存并强制刷新(Ctrl+F5)
|
|
||||||
2. **Redis缓存**:清理Redis中的菜单缓存
|
|
||||||
3. **菜单配置**:检查是否有其他SQL脚本重复执行了菜单配置
|
|
||||||
|
|
||||||
## 📞 需要帮助?
|
|
||||||
|
|
||||||
如果问题仍未解决,请提供:
|
|
||||||
- 执行检查脚本的输出结果
|
|
||||||
- 浏览器控制台的错误信息
|
|
||||||
- 清理脚本的执行结果
|
|
||||||
|
|
@ -46,54 +46,108 @@ WHERE t1.menu_name = '心理网站管理'
|
||||||
AND t2.parent_id = 0
|
AND t2.parent_id = 0
|
||||||
AND t1.menu_id > t2.menu_id;
|
AND t1.menu_id > t2.menu_id;
|
||||||
|
|
||||||
-- 删除其他重复菜单(基于path和component)
|
-- 删除基于menu_name和parent_id的重复菜单(优先处理)
|
||||||
DELETE t1 FROM sys_menu t1
|
DELETE t1 FROM sys_menu t1
|
||||||
INNER JOIN sys_menu t2
|
INNER JOIN sys_menu t2
|
||||||
WHERE t1.path = t2.path
|
WHERE t1.menu_name = t2.menu_name
|
||||||
AND t1.component = t2.component
|
AND t1.parent_id = t2.parent_id
|
||||||
AND t1.menu_name = t2.menu_name
|
|
||||||
AND t1.menu_id > t2.menu_id
|
AND t1.menu_id > t2.menu_id
|
||||||
AND (t1.menu_name LIKE '%心理%'
|
AND (t1.menu_name LIKE '%心理%'
|
||||||
OR t1.menu_name LIKE '%量表%'
|
OR t1.menu_name LIKE '%量表%'
|
||||||
OR t1.menu_name LIKE '%题目%'
|
OR t1.menu_name LIKE '%题目%'
|
||||||
OR t1.menu_name LIKE '%因子%'
|
OR t1.menu_name LIKE '%因子%'
|
||||||
OR t1.menu_name LIKE '%测评%'
|
OR t1.menu_name LIKE '%测评%'
|
||||||
|
OR t1.menu_name LIKE '%报告%'
|
||||||
|
OR t1.menu_name LIKE '%解释%'
|
||||||
|
OR t1.menu_name LIKE '%档案%'
|
||||||
|
OR t1.menu_name LIKE '%问卷%'
|
||||||
OR t1.menu_name LIKE '%网站%'
|
OR t1.menu_name LIKE '%网站%'
|
||||||
OR t1.menu_name LIKE '%栏目%'
|
OR t1.menu_name LIKE '%栏目%'
|
||||||
OR t1.menu_name LIKE '%评论%'
|
OR t1.menu_name LIKE '%评论%'
|
||||||
OR t1.menu_name LIKE '%预警%'
|
OR t1.menu_name LIKE '%预警%'
|
||||||
|
OR t1.menu_name LIKE '%规则%');
|
||||||
|
|
||||||
|
-- 删除其他重复菜单(基于path和component)
|
||||||
|
DELETE t1 FROM sys_menu t1
|
||||||
|
INNER JOIN sys_menu t2
|
||||||
|
WHERE t1.path = t2.path
|
||||||
|
AND (t1.component = t2.component OR (t1.component IS NULL AND t2.component IS NULL))
|
||||||
|
AND t1.menu_name = t2.menu_name
|
||||||
|
AND t1.parent_id = t2.parent_id
|
||||||
|
AND t1.menu_id > t2.menu_id
|
||||||
|
AND (t1.menu_name LIKE '%心理%'
|
||||||
|
OR t1.menu_name LIKE '%量表%'
|
||||||
|
OR t1.menu_name LIKE '%题目%'
|
||||||
|
OR t1.menu_name LIKE '%因子%'
|
||||||
|
OR t1.menu_name LIKE '%测评%'
|
||||||
|
OR t1.menu_name LIKE '%报告%'
|
||||||
|
OR t1.menu_name LIKE '%解释%'
|
||||||
|
OR t1.menu_name LIKE '%档案%'
|
||||||
OR t1.menu_name LIKE '%问卷%'
|
OR t1.menu_name LIKE '%问卷%'
|
||||||
OR t1.menu_name LIKE '%档案%');
|
OR t1.menu_name LIKE '%网站%'
|
||||||
|
OR t1.menu_name LIKE '%栏目%'
|
||||||
|
OR t1.menu_name LIKE '%评论%'
|
||||||
|
OR t1.menu_name LIKE '%预警%'
|
||||||
|
OR t1.menu_name LIKE '%规则%');
|
||||||
|
|
||||||
-- ========================================
|
-- ========================================
|
||||||
-- 3. 清理孤立的菜单(parent_id指向已删除的菜单)
|
-- 3. 清理孤立的子菜单(父菜单已被删除)
|
||||||
-- ========================================
|
-- ========================================
|
||||||
-- 先删除父菜单被删除但子菜单还存在的情况(应该先执行步骤2再执行这一步)
|
-- 删除那些父菜单ID不存在于sys_menu表中的子菜单
|
||||||
-- 注意:这一步需要确保先执行步骤2,否则可能会误删
|
DELETE FROM sys_menu
|
||||||
|
WHERE parent_id > 0
|
||||||
-- 暂时注释掉,因为MySQL不允许在同一个表中删除和查询
|
AND parent_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp)
|
||||||
-- 如果需要清理孤立菜单,建议手动检查:
|
AND (menu_name LIKE '%心理%'
|
||||||
-- SELECT * FROM sys_menu WHERE parent_id NOT IN (SELECT menu_id FROM sys_menu)
|
OR menu_name LIKE '%量表%'
|
||||||
-- AND menu_name LIKE '%心理%';
|
OR menu_name LIKE '%题目%'
|
||||||
|
OR menu_name LIKE '%因子%'
|
||||||
|
OR menu_name LIKE '%测评%'
|
||||||
|
OR menu_name LIKE '%报告%'
|
||||||
|
OR menu_name LIKE '%解释%'
|
||||||
|
OR menu_name LIKE '%档案%'
|
||||||
|
OR menu_name LIKE '%问卷%'
|
||||||
|
OR menu_name LIKE '%网站%'
|
||||||
|
OR menu_name LIKE '%栏目%'
|
||||||
|
OR menu_name LIKE '%评论%'
|
||||||
|
OR menu_name LIKE '%预警%'
|
||||||
|
OR menu_name LIKE '%规则%');
|
||||||
|
|
||||||
-- ========================================
|
-- ========================================
|
||||||
-- 4. 验证清理结果
|
-- 4. 清理角色菜单关联表中的孤立记录
|
||||||
-- ========================================
|
-- ========================================
|
||||||
SELECT '清理完成!当前心理学相关菜单数量:' AS result;
|
-- 删除指向已删除菜单的角色菜单关联
|
||||||
SELECT menu_name, path, component, COUNT(*) as count
|
DELETE FROM sys_role_menu
|
||||||
|
WHERE menu_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp2);
|
||||||
|
|
||||||
|
-- ========================================
|
||||||
|
-- 5. 验证清理结果
|
||||||
|
-- ========================================
|
||||||
|
SELECT '清理完成!' AS result;
|
||||||
|
|
||||||
|
-- 检查是否还有重复菜单
|
||||||
|
SELECT
|
||||||
|
menu_name AS '菜单名称',
|
||||||
|
path AS '路由路径',
|
||||||
|
component AS '组件路径',
|
||||||
|
parent_id AS '父菜单ID',
|
||||||
|
COUNT(*) AS '剩余数量'
|
||||||
FROM sys_menu
|
FROM sys_menu
|
||||||
WHERE menu_name LIKE '%心理%'
|
WHERE menu_name LIKE '%心理%'
|
||||||
OR menu_name LIKE '%量表%'
|
OR menu_name LIKE '%量表%'
|
||||||
OR menu_name LIKE '%题目%'
|
OR menu_name LIKE '%题目%'
|
||||||
OR menu_name LIKE '%因子%'
|
OR menu_name LIKE '%因子%'
|
||||||
OR menu_name LIKE '%测评%'
|
OR menu_name LIKE '%测评%'
|
||||||
|
OR menu_name LIKE '%报告%'
|
||||||
|
OR menu_name LIKE '%解释%'
|
||||||
|
OR menu_name LIKE '%档案%'
|
||||||
|
OR menu_name LIKE '%问卷%'
|
||||||
OR menu_name LIKE '%网站%'
|
OR menu_name LIKE '%网站%'
|
||||||
OR menu_name LIKE '%栏目%'
|
OR menu_name LIKE '%栏目%'
|
||||||
OR menu_name LIKE '%评论%'
|
OR menu_name LIKE '%评论%'
|
||||||
OR menu_name LIKE '%预警%'
|
OR menu_name LIKE '%预警%'
|
||||||
OR menu_name LIKE '%问卷%'
|
OR menu_name LIKE '%规则%'
|
||||||
OR menu_name LIKE '%档案%'
|
GROUP BY menu_name, path, component, parent_id
|
||||||
GROUP BY menu_name, path, component
|
HAVING COUNT(*) > 1;
|
||||||
HAVING count > 1;
|
|
||||||
|
|
||||||
-- 如果没有输出,说明没有重复菜单了
|
-- 如果没有输出,说明没有重复菜单了
|
||||||
|
SELECT '如果上面的查询没有返回结果,说明所有重复菜单已清理完成!' AS message;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user