管理员能管理用户,用户也能选择量表进行测量,然后也能出现结果。但是所有操作都是在管理员的界面操作的
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
|
||||
|
||||
cd bin
|
||||
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 = [
|
||||
// 系统管理菜单
|
||||
{
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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("测评已提交");
|
||||
submitAssessment(this.assessmentId).then(response => {
|
||||
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 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("加载报告失败,请检查报告是否存在");
|
||||
});
|
||||
},
|
||||
/** 返回 */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存答案
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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配置
|
||||
|
|
|
|||
|
|
@ -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 对象池 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
// 配置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)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 不抛出异常,返回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" })
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
* 查询结果解释列表
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
/**
|
||||
* 新增预警规则
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user