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

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
cd bin
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 = [
// 系统管理菜单
{
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',
component: Layout,
@ -262,6 +287,17 @@ export const dynamicRoutes = [
roles: ['admin']
}
},
// 报告详情
{
path: 'report/detail',
name: 'ReportDetail',
component: () => import('@/views/psychology/report/detail'),
hidden: true,
meta: {
title: '报告详情',
roles: ['admin']
}
},
// 解释配置
{
path: 'interpretation',

View File

@ -76,7 +76,7 @@
</template>
<script>
import { startAssessment, pausedAssessmentList } from "@/api/psychology/assessment";
import { startAssessment, pausedAssessmentList, resumeAssessment } from "@/api/psychology/assessment";
import { listScale } from "@/api/psychology/scale";
import { listProfile } from "@/api/psychology/profile";
@ -183,7 +183,18 @@ export default {
},
/** 继续测评 */
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() {

View File

@ -61,13 +61,15 @@
<div class="navigation-buttons">
<el-button @click="handlePrev" :disabled="currentIndex === 0">上一题</el-button>
<el-button type="primary" @click="handleNext" :disabled="currentIndex === totalItems - 1">下一题</el-button>
<el-button type="success" @click="handleSubmit" :disabled="!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>
</template>
<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 { saveAnswer } from "@/api/psychology/assessment";
@ -99,8 +101,27 @@ export default {
progressPercent() {
return this.totalItems > 0 ? Math.round((this.currentIndex + 1) / this.totalItems * 100) : 0;
},
answeredCount() {
//
return this.itemList.filter(item => {
const answer = this.answersMap[item.itemId];
if (!answer) {
return false;
}
// 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() {
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() {
@ -118,16 +139,50 @@ export default {
this.loading = true;
Promise.all([
getAssessment(this.assessmentId),
getAssessmentItems(this.assessmentId)
]).then(([assessmentRes, itemsRes]) => {
getAssessmentItems(this.assessmentId),
getAssessmentAnswers(this.assessmentId)
]).then(([assessmentRes, itemsRes, answersRes]) => {
const assessment = assessmentRes.data;
if (!assessment) {
this.$modal.msgError("测评不存在");
this.$router.push('/psychology/assessment');
return;
}
this.scaleName = assessment.scaleName || '未知量表';
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;
}).catch(() => {
}).catch(error => {
console.error('加载测评信息失败:', error);
this.$modal.msgError("加载测评信息失败,请重试");
this.loading = false;
});
},
@ -138,7 +193,7 @@ export default {
this.$set(this.optionMap, item.itemId, response.data || []);
});
});
Promise.all(promises);
return Promise.all(promises);
},
/** 答案改变事件 */
handleAnswerChange() {
@ -158,10 +213,21 @@ export default {
if (selectedOpt) {
answer.answerScore = selectedOpt.optionScore || 0;
}
this.answersMap[itemId] = answer;
// 使 $set
this.$set(this.answersMap, itemId, answer);
} else if (this.currentItem.itemType === 'multiple') {
answer.optionIds = this.selectedOptions.join(',');
this.answersMap[itemId] = answer;
answer.optionIds = this.selectedOptions.length > 0 ? this.selectedOptions.join(',') : null;
//
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) {
saveAnswer(answer).then(() => {
//
//
}).catch(error => {
console.error('保存答案失败:', error);
//
});
},
/** 上一题 */
@ -189,15 +258,26 @@ export default {
},
/** 加载当前题目的答案 */
loadCurrentAnswer() {
if (!this.currentItem) {
return;
}
const itemId = this.currentItem.itemId;
const answer = this.answersMap[itemId];
if (this.currentItem.itemType === 'single') {
this.selectedOption = answer ? answer.optionId : null;
this.selectedOption = answer && answer.optionId ? answer.optionId : null;
this.selectedOptions = [];
} else if (this.currentItem.itemType === 'multiple') {
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() {
//
if (!this.isComplete) {
const remaining = this.itemList.length - this.answeredCount;
this.$modal.msgWarning(`还有 ${remaining} 道题目未作答,请完成所有题目后再提交`);
return;
}
this.$modal.confirm('确定要提交测评吗?提交后将不能修改。').then(() => {
this.$modal.msgSuccess("测评已提交");
this.$router.push('/psychology/assessment');
submitAssessment(this.assessmentId).then(response => {
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 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);
loadFunc.then(response => {
this.reportForm = response.data || {};
if (response.data) {
this.reportForm = response.data;
} else {
this.$modal.msgWarning("报告不存在");
}
this.loading = false;
}).catch(() => {
}).catch(error => {
this.loading = false;
this.$modal.msgError("加载报告失败");
console.error('加载报告失败:', error);
this.$modal.msgError("加载报告失败,请检查报告是否存在");
});
},
/** 返回 */

View File

@ -171,7 +171,7 @@ export default {
},
/** 查看按钮操作 */
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) {

View File

@ -191,6 +191,16 @@ public class PsyAssessmentController extends BaseController
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:
# 名称
name: 动动脑新闻系统
@ -77,7 +77,8 @@ spring:
# password: xbZttkmndxCkWsycjs2
# 连接超时时间
timeout: 10s
lettuce:
# 使用Jedis客户端替代Lettuce客户端
jedis:
pool:
# 连接池中的最小空闲连接
min-idle: 0
@ -85,7 +86,7 @@ spring:
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 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"
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">
@ -99,6 +99,19 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<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>
<!-- pool 对象池 -->

View File

@ -1,9 +1,11 @@
package com.ddnai.common.core.domain.model;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONField;
import com.ddnai.common.core.domain.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
@ -12,66 +14,82 @@ import java.util.Set;
*
* @author ddnai
*/
public class LoginUser implements UserDetails
public class LoginUser implements UserDetails, Serializable, Cloneable
{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@JSONField(name = "userId", ordinal = 1)
private Long userId;
/**
* 部门ID
*/
@JSONField(name = "deptId", ordinal = 2)
private Long deptId;
/**
* 用户唯一标识
*/
@JSONField(name = "token", ordinal = 3)
private String token;
/**
* 登录时间
*/
@JSONField(name = "loginTime", ordinal = 4)
private Long loginTime;
/**
* 过期时间
*/
@JSONField(name = "expireTime", ordinal = 5)
private Long expireTime;
/**
* 登录IP地址
*/
@JSONField(name = "ipaddr", ordinal = 6)
private String ipaddr;
/**
* 登录地点
*/
@JSONField(name = "loginLocation", ordinal = 7)
private String loginLocation;
/**
* 浏览器类型
*/
@JSONField(name = "browser", ordinal = 8)
private String browser;
/**
* 操作系统
*/
@JSONField(name = "os", ordinal = 9)
private String os;
/**
* 权限列表
*/
@JSONField(name = "permissions", ordinal = 10)
private Set<String> permissions;
/**
* 用户信息
*/
@JSONField(name = "user")
@JSONField(name = "user", ordinal = 11, serialize = true, deserialize = true)
private SysUser user;
/**
* 用户名
*/
@JSONField(name = "username", ordinal = 12)
private String username;
public LoginUser()
{
}
@ -80,6 +98,10 @@ public class LoginUser implements UserDetails
{
this.user = user;
this.permissions = permissions;
// 同步用户名
if (user != null && user.getUserName() != null) {
this.username = user.getUserName();
}
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
@ -88,6 +110,10 @@ public class LoginUser implements UserDetails
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
// 同步用户名
if (user != null && user.getUserName() != null) {
this.username = user.getUserName();
}
}
public Long getUserId()
@ -120,6 +146,20 @@ public class LoginUser implements UserDetails
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)
@Override
public String getPassword()
@ -127,12 +167,6 @@ public class LoginUser implements UserDetails
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
public boolean isEnabled()
{
return true;
return user != null && "0".equals(user.getStatus());
}
public Long getLoginTime()
@ -257,6 +291,10 @@ public class LoginUser implements UserDetails
public void setUser(SysUser user)
{
this.user = user;
// 同步用户名
if (user != null && user.getUserName() != null) {
this.username = user.getUserName();
}
}
@Override
@ -264,5 +302,31 @@ public class LoginUser implements UserDetails
{
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;
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.LoggerFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import com.ddnai.common.constant.Constants;
import com.ddnai.common.core.domain.model.LoginUser;
/**
* Redis使用FastJson序列化
*
* @author ddnai
* @author ruoyi
*/
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");
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
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)
{
super();
@ -39,12 +52,11 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
return new byte[0];
}
try
{
try {
// 使用基本的序列化配置
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
catch (Exception e)
{
} catch (Exception e) {
log.error("序列化对象失败: {}", e.getMessage(), e);
throw new SerializationException("序列化对象失败", e);
}
@ -57,23 +69,60 @@ public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
return null;
}
try
{
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
}
catch (Exception e)
{
log.error("反序列化对象失败,类型: {}, 错误: {}", clazz.getName(), e.getMessage());
// 记录前200个字符的JSON内容便于调试
if (bytes.length > 0)
{
String preview = new String(bytes, 0, Math.min(200, bytes.length), DEFAULT_CHARSET);
log.error("JSON内容预览: {}", preview);
String str = null;
try {
str = new String(bytes, DEFAULT_CHARSET);
// 对于LoginUser类型使用更安全的反序列化方式
if (isLoginUserClass(clazz) || str.contains("LoginUser")) {
log.debug("处理LoginUser类型的反序列化");
try {
return JSON.parseObject(str, clazz);
} catch (ClassCastException e) {
// 如果出现类型转换异常抛出SerializationException以便上层处理
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" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
// 创建RedisTemplate实例
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer作为key的序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
// 使用FastJson2JsonRedisSerializer作为value的序列化器
// 这个序列化器已经配置了安全的类型处理和白名单
FastJson2JsonRedisSerializer<Object> fastJsonSerializer = new FastJson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(fastJsonSerializer);
template.setHashValueSerializer(fastJsonSerializer);
// 初始化RedisTemplate
template.afterPropertiesSet();
return template;
}

View File

@ -65,26 +65,67 @@ public class TokenService
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
String uuid = null;
String userKey = null;
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
if (StringUtils.isEmpty(uuid))
{
return null;
}
userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
// 验证用户对象是否完整
if (user != null && user.getUser() == null)
{
log.warn("用户缓存数据不完整token: {}", uuid);
log.warn("用户缓存数据不完整token: {},删除损坏的缓存", uuid);
// 删除损坏的缓存
redisCache.deleteObject(userKey);
return null;
}
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)
{
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;

View File

@ -21,6 +21,9 @@ public class PsyAssessment extends BaseEntity
/** 量表ID */
private Long scaleId;
/** 量表名称(关联查询字段,不存储在表中) */
private String scaleName;
/** 用户ID */
private Long userId;
@ -96,6 +99,16 @@ public class PsyAssessment extends BaseEntity
this.scaleId = scaleId;
}
public String getScaleName()
{
return scaleName;
}
public void setScaleName(String scaleName)
{
this.scaleName = scaleName;
}
public Long getUserId()
{
return userId;

View File

@ -1,6 +1,7 @@
package com.ddnai.system.mapper.psychology;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ddnai.system.domain.psychology.PsyResultInterpretation;
/**
@ -26,7 +27,7 @@ public interface PsyResultInterpretationMapper
* @param score 得分
* @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;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ddnai.system.domain.psychology.PsyWarningRule;
/**
@ -41,7 +42,7 @@ public interface PsyWarningRuleMapper
* @param factorId 因子ID为空表示总分配置
* @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())
</foreach>
on duplicate key update
option_id = values(option_id),
option_ids = values(option_ids),
answer_score = values(answer_score),
answer_text = values(answer_text),
option_id = VALUES(option_id),
option_ids = VALUES(option_ids),
answer_score = VALUES(answer_score),
answer_text = VALUES(answer_text),
create_time = sysdate()
</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">
<result property="assessmentId" column="assessment_id" />
<result property="scaleId" column="scale_id" />
<result property="scaleName" column="scale_name" />
<result property="userId" column="user_id" />
<result property="assesseeName" column="assessee_name" />
<result property="assesseeGender" column="assessee_gender" />
@ -32,47 +33,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectAssessmentVo">
select assessment_id, scale_id, user_id, assessee_name, assessee_gender, assessee_age,
assessee_id_card, assessee_phone, assessee_email, start_time, pause_time,
resume_time, pause_count, submit_time, complete_time, total_score, status,
ip_address, user_agent, create_by, create_time, update_by, update_time, remark
from psy_assessment
select a.assessment_id, a.scale_id, s.scale_name, a.user_id, a.assessee_name, a.assessee_gender, a.assessee_age,
a.assessee_id_card, a.assessee_phone, a.assessee_email, a.start_time, a.pause_time,
a.resume_time, a.pause_count, a.submit_time, a.complete_time, a.total_score, a.status,
a.ip_address, a.user_agent, a.create_by, a.create_time, a.update_by, a.update_time, a.remark
from psy_assessment a
left join psy_scale s on a.scale_id = s.scale_id
</sql>
<select id="selectAssessmentById" parameterType="Long" resultMap="PsyAssessmentResult">
<include refid="selectAssessmentVo"/>
where assessment_id = #{assessmentId}
where a.assessment_id = #{assessmentId}
</select>
<select id="selectAssessmentList" parameterType="com.ddnai.system.domain.psychology.PsyAssessment" resultMap="PsyAssessmentResult">
<include refid="selectAssessmentVo"/>
<where>
<if test="scaleId != null">
AND scale_id = #{scaleId}
AND a.scale_id = #{scaleId}
</if>
<if test="userId != null">
AND user_id = #{userId}
AND a.user_id = #{userId}
</if>
<if test="assesseeName != null and assesseeName != ''">
AND assessee_name like concat('%', #{assesseeName}, '%')
AND a.assessee_name like concat('%', #{assesseeName}, '%')
</if>
<if test="status != null and status != ''">
AND status = #{status}
AND a.status = #{status}
</if>
</where>
order by create_time desc
order by a.create_time desc
</select>
<select id="selectAssessmentListByUserId" parameterType="Long" resultMap="PsyAssessmentResult">
<include refid="selectAssessmentVo"/>
where user_id = #{userId}
order by create_time desc
where a.user_id = #{userId}
order by a.create_time desc
</select>
<select id="selectPausedAssessmentList" parameterType="Long" resultMap="PsyAssessmentResult">
<include refid="selectAssessmentVo"/>
where user_id = #{userId} and status = '3'
order by pause_time desc
where a.user_id = #{userId} and a.status = '3'
order by a.pause_time desc
</select>
<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="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectReportVo">
select report_id, assessment_id, report_type, report_title, report_content, summary,
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
</sql>
@ -66,7 +65,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pdfPath != null and pdfPath != ''">pdf_path, </if>
<if test="isGenerated != null and isGenerated != ''">is_generated, </if>
<if test="generateTime != null">generate_time, </if>
<if test="remark != null">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
@ -79,7 +77,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pdfPath != null and pdfPath != ''">#{pdfPath}, </if>
<if test="isGenerated != null and isGenerated != ''">#{isGenerated}, </if>
<if test="generateTime != null">#{generateTime}, </if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)

View File

@ -28,30 +28,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectProfileVo">
select profile_id, user_id, profile_type, profile_data, avatar, id_card, user_name, phone, birthday,
education, occupation, address, emergency_contact, emergency_phone,
medical_history, create_by, create_time, update_by, update_time, remark
from psy_user_profile
select p.profile_id, p.user_id, p.profile_type, p.profile_data, p.avatar, p.id_card, p.birthday,
p.education, p.occupation, p.address, p.emergency_contact, p.emergency_phone,
p.medical_history, p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
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>
<select id="selectProfileById" parameterType="Long" resultMap="PsyUserProfileResult">
<include refid="selectProfileVo"/>
where profile_id = #{profileId}
where p.profile_id = #{profileId}
</select>
<select id="selectProfileByUserId" parameterType="Long" resultMap="PsyUserProfileResult">
<include refid="selectProfileVo"/>
where user_id = #{userId}
where p.user_id = #{userId}
</select>
<select id="selectProfileList" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" resultMap="PsyUserProfileResult">
<include refid="selectProfileVo"/>
<where>
<if test="userId != null"> and user_id = #{userId}</if>
<if test="profileType != null and profileType != ''"> and profile_type = #{profileType}</if>
<if test="idCard != null and idCard != ''"> and id_card = #{idCard}</if>
<if test="userId != null"> and p.user_id = #{userId}</if>
<if test="profileType != null and profileType != ''"> and p.profile_type = #{profileType}</if>
<if test="idCard != null and idCard != ''"> and p.id_card = #{idCard}</if>
</where>
order by create_time desc
order by p.create_time desc
</select>
<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="avatar != null">avatar, </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="education != null">education, </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="avatar != null">#{avatar}, </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="education != null">#{education}, </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="avatar != null">avatar = #{avatar}, </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="education != null">education = #{education}, </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 t1.menu_id > t2.menu_id;
-- 删除其他重复菜单基于path和component
-- 删除基于menu_name和parent_id的重复菜单优先处理
DELETE t1 FROM sys_menu t1
INNER JOIN sys_menu t2
WHERE t1.path = t2.path
AND t1.component = t2.component
AND t1.menu_name = t2.menu_name
WHERE t1.menu_name = t2.menu_name
AND t1.parent_id = t2.parent_id
AND t1.menu_id > t2.menu_id
AND (t1.menu_name LIKE '%心理%'
OR t1.menu_name LIKE '%量表%'
OR t1.menu_name LIKE '%题目%'
OR t1.menu_name LIKE '%因子%'
OR t1.menu_name LIKE '%测评%'
OR t1.menu_name LIKE '%报告%'
OR t1.menu_name LIKE '%解释%'
OR t1.menu_name LIKE '%档案%'
OR t1.menu_name LIKE '%问卷%'
OR t1.menu_name LIKE '%网站%'
OR t1.menu_name LIKE '%栏目%'
OR t1.menu_name LIKE '%评论%'
OR t1.menu_name LIKE '%预警%'
OR t1.menu_name LIKE '%规则%');
-- 删除其他重复菜单基于path和component
DELETE t1 FROM sys_menu t1
INNER JOIN sys_menu t2
WHERE t1.path = t2.path
AND (t1.component = t2.component OR (t1.component IS NULL AND t2.component IS NULL))
AND t1.menu_name = t2.menu_name
AND t1.parent_id = t2.parent_id
AND t1.menu_id > t2.menu_id
AND (t1.menu_name LIKE '%心理%'
OR t1.menu_name LIKE '%量表%'
OR t1.menu_name LIKE '%题目%'
OR t1.menu_name LIKE '%因子%'
OR t1.menu_name LIKE '%测评%'
OR t1.menu_name LIKE '%报告%'
OR t1.menu_name LIKE '%解释%'
OR t1.menu_name LIKE '%档案%'
OR t1.menu_name LIKE '%问卷%'
OR t1.menu_name LIKE '%档案%');
OR t1.menu_name LIKE '%网站%'
OR t1.menu_name LIKE '%栏目%'
OR t1.menu_name LIKE '%评论%'
OR t1.menu_name LIKE '%预警%'
OR t1.menu_name LIKE '%规则%');
-- ========================================
-- 3. 清理孤立的菜单parent_id指向已删除的菜单
-- 3. 清理孤立的子菜单(父菜单已被删除
-- ========================================
-- 先删除父菜单被删除但子菜单还存在的情况应该先执行步骤2再执行这一步
-- 注意这一步需要确保先执行步骤2否则可能会误删
-- 暂时注释掉因为MySQL不允许在同一个表中删除和查询
-- 如果需要清理孤立菜单,建议手动检查:
-- SELECT * FROM sys_menu WHERE parent_id NOT IN (SELECT menu_id FROM sys_menu)
-- AND menu_name LIKE '%心理%';
-- 删除那些父菜单ID不存在于sys_menu表中的子菜单
DELETE FROM sys_menu
WHERE parent_id > 0
AND parent_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp)
AND (menu_name LIKE '%心理%'
OR menu_name LIKE '%量表%'
OR menu_name LIKE '%题目%'
OR menu_name LIKE '%因子%'
OR menu_name LIKE '%测评%'
OR menu_name LIKE '%报告%'
OR menu_name LIKE '%解释%'
OR menu_name LIKE '%档案%'
OR menu_name LIKE '%问卷%'
OR menu_name LIKE '%网站%'
OR menu_name LIKE '%栏目%'
OR menu_name LIKE '%评论%'
OR menu_name LIKE '%预警%'
OR menu_name LIKE '%规则%');
-- ========================================
-- 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
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 '%档案%'
GROUP BY menu_name, path, component
HAVING count > 1;
OR menu_name LIKE '%规则%'
GROUP BY menu_name, path, component, parent_id
HAVING COUNT(*) > 1;
-- 如果没有输出,说明没有重复菜单了
SELECT '如果上面的查询没有返回结果,说明所有重复菜单已清理完成!' AS message;