管理员能管理用户,用户也能选择量表进行测量,然后也能出现结果。但是所有操作都是在管理员的界面操作的

This commit is contained in:
xiao@123.123 2025-11-06 16:47:19 +08:00
parent 8ec233f820
commit ed835d628c
23 changed files with 550 additions and 209 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 || "提交失败,请重试");
});
}); });
} }
}, },

View File

@ -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("加载报告失败,请检查报告是否存在");
}); });
}, },
/** 返回 */ /** 返回 */

View File

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

View File

@ -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);
}
/** /**
* 保存答案 * 保存答案
*/ */

View File

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

View File

@ -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 对象池 -->

View File

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

View File

@ -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
{
// 配置ObjectReaderProviderFastJSON2 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);
}
} }

View File

@ -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();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值 // 使用FastJson2JsonRedisSerializer作为value的序列化器
template.setKeySerializer(new StringRedisSerializer()); // 这个序列化器已经配置了安全的类型处理和白名单
template.setValueSerializer(serializer); FastJson2JsonRedisSerializer<Object> fastJsonSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(fastJsonSerializer);
// Hash的key也采用StringRedisSerializer的序列化方式 template.setHashValueSerializer(fastJsonSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
// 初始化RedisTemplate
template.afterPropertiesSet(); template.afterPropertiesSet();
return template; return template;
} }

View File

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

View File

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

View File

@ -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);
/** /**
* 查询结果解释列表 * 查询结果解释列表

View File

@ -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);
/** /**
* 新增预警规则 * 新增预警规则

View File

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

View File

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

View File

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

View File

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

View File

@ -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脚本重复执行了菜单配置
## 📞 需要帮助?
如果问题仍未解决,请提供:
- 执行检查脚本的输出结果
- 浏览器控制台的错误信息
- 清理脚本的执行结果

View File

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