2025年11月6日

This commit is contained in:
xiao@123.123 2025-11-06 18:06:15 +08:00
parent ed835d628c
commit f4cef1779c
27 changed files with 3394 additions and 65 deletions

View File

@ -0,0 +1,73 @@
import request from '@/utils/request'
// 查询权限列表
export function listPermission(query) {
return request({
url: '/psychology/permission/list',
method: 'get',
params: query
})
}
// 查询权限详细
export function getPermission(permissionId) {
return request({
url: '/psychology/permission/' + permissionId,
method: 'get'
})
}
// 根据用户ID获取用户有权限访问的量表ID列表
export function getUserScaleIds(userId) {
return request({
url: '/psychology/permission/user/' + userId + '/scales',
method: 'get'
})
}
// 检查用户是否有权限访问指定量表
export function checkPermission(userId, scaleId) {
return request({
url: '/psychology/permission/check/' + userId + '/' + scaleId,
method: 'get'
})
}
// 新增权限
export function addPermission(data) {
return request({
url: '/psychology/permission',
method: 'post',
data: data
})
}
// 修改权限
export function updatePermission(data) {
return request({
url: '/psychology/permission',
method: 'put',
data: data
})
}
// 删除权限
export function delPermission(permissionIds) {
return request({
url: '/psychology/permission/' + permissionIds,
method: 'delete'
})
}
// 批量分配用户量表权限
export function assignUserScales(userId, scaleIds) {
return request({
url: '/psychology/permission/assign',
method: 'post',
data: {
userId: userId,
scaleIds: scaleIds
}
})
}

View File

@ -298,6 +298,28 @@ export const dynamicRoutes = [
roles: ['admin']
}
},
// 量表权限管理
{
path: 'permission',
name: 'ScalePermission',
component: () => import('@/views/psychology/permission/index'),
meta: {
title: '量表权限管理',
icon: 'lock',
roles: ['admin']
}
},
// 用户量表权限分配
{
path: 'permission/user/:userId',
name: 'UserScalePermission',
component: () => import('@/views/psychology/permission/user'),
hidden: true,
meta: {
title: '分配量表权限',
roles: ['admin']
}
},
// 解释配置
{
path: 'interpretation',

View File

@ -91,8 +91,8 @@ export default {
loading: false,
//
captchaEnabled: true,
//
register: false,
//
register: true,
redirect: undefined
}
},

View File

@ -110,9 +110,34 @@ export default {
methods: {
/** 加载量表列表 */
loadScales() {
listScale({ status: '0' }).then(response => {
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
});
// ID
const userId = this.$store.getters.userId;
//
if (userId === 1) {
//
listScale({ status: '0' }).then(response => {
this.scaleList = response.rows.filter(scale => scale.itemCount > 0);
});
} else {
//
import('@/api/psychology/permission').then(module => {
module.getUserScaleIds(userId).then(permissionResponse => {
const allowedScaleIds = permissionResponse.data || [];
if (allowedScaleIds.length === 0) {
this.scaleList = [];
this.$message.warning('您还没有被分配任何量表权限,请联系管理员');
return;
}
//
listScale({ status: '0' }).then(response => {
this.scaleList = response.rows.filter(scale =>
scale.itemCount > 0 && allowedScaleIds.includes(scale.scaleId)
);
});
});
});
}
},
/** 加载用户档案列表 */
loadProfiles() {

View File

@ -1,21 +1,39 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="量表ID" prop="scaleId">
<el-input
<el-form-item label="量表" prop="scaleId">
<el-select
v-model="queryParams.scaleId"
placeholder="请输入量表ID"
placeholder="请选择量表"
clearable
@keyup.enter.native="handleQuery"
/>
filterable
style="width: 200px;"
@change="handleScaleChange"
>
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:value="scale.scaleId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="因子ID" prop="factorId">
<el-input
<el-form-item label="因子" prop="factorId">
<el-select
v-model="queryParams.factorId"
placeholder="请输入因子ID"
placeholder="请选择因子"
clearable
@keyup.enter.native="handleQuery"
/>
filterable
style="width: 200px;"
:disabled="!queryParams.scaleId"
>
<el-option
v-for="factor in factorList"
:key="factor.factorId"
:label="factor.factorName"
:value="factor.factorId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="等级" prop="level">
<el-input
@ -70,8 +88,16 @@
<el-table v-loading="loading" :data="interpretationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="interpretationId" width="80" />
<el-table-column label="量表ID" align="center" prop="scaleId" width="100" />
<el-table-column label="因子ID" align="center" prop="factorId" width="100" />
<el-table-column label="量表" align="center" width="150">
<template slot-scope="scope">
<span>{{ getScaleName(scope.row.scaleId) || '通用' }}</span>
</template>
</el-table-column>
<el-table-column label="因子" align="center" width="150">
<template slot-scope="scope">
<span>{{ getFactorName(scope.row.factorId) || '总体' }}</span>
</template>
</el-table-column>
<el-table-column label="分数范围" align="center" width="150">
<template slot-scope="scope">
{{ scope.row.scoreRangeMin }} - {{ scope.row.scoreRangeMax }}
@ -118,11 +144,39 @@
<!-- 添加或修改对话框 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="量表ID" prop="scaleId">
<el-input v-model="form.scaleId" placeholder="留空表示通用解释" />
<el-form-item label="量表" prop="scaleId">
<el-select
v-model="form.scaleId"
placeholder="请选择量表(留空表示通用解释)"
clearable
filterable
style="width: 100%;"
@change="handleFormScaleChange"
>
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:value="scale.scaleId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="因子ID" prop="factorId">
<el-input v-model="form.factorId" placeholder="留空表示总体解释" />
<el-form-item label="因子" prop="factorId">
<el-select
v-model="form.factorId"
placeholder="请选择因子(留空表示总体解释)"
clearable
filterable
style="width: 100%;"
:disabled="!form.scaleId"
>
<el-option
v-for="factor in formFactorList"
:key="factor.factorId"
:label="factor.factorName"
:value="factor.factorId">
</el-option>
</el-select>
</el-form-item>
<el-row>
<el-col :span="12">
@ -164,8 +218,8 @@
<!-- 查看对话框 -->
<el-dialog title="解释详情" :visible.sync="viewOpen" width="800px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="量表ID">{{ viewForm.scaleId || '通用' }}</el-descriptions-item>
<el-descriptions-item label="因子ID">{{ viewForm.factorId || '总体' }}</el-descriptions-item>
<el-descriptions-item label="量表">{{ getScaleName(viewForm.scaleId) || '通用' }}</el-descriptions-item>
<el-descriptions-item label="因子">{{ getFactorName(viewForm.factorId) || '总体' }}</el-descriptions-item>
<el-descriptions-item label="分数范围">{{ viewForm.scoreRangeMin }} - {{ viewForm.scoreRangeMax }}</el-descriptions-item>
<el-descriptions-item label="等级">{{ viewForm.level }}</el-descriptions-item>
<el-descriptions-item label="等级名称">{{ viewForm.levelName }}</el-descriptions-item>
@ -186,6 +240,8 @@
<script>
import { listInterpretation, getInterpretation, addInterpretation, updateInterpretation, delInterpretation } from "@/api/psychology/interpretation";
import { listScale } from "@/api/psychology/scale";
import { listFactor } from "@/api/psychology/factor";
export default {
name: "Interpretation",
@ -223,6 +279,12 @@ export default {
form: {},
//
viewForm: {},
//
scaleList: [],
//
factorList: [],
//
formFactorList: [],
//
rules: {
interpretationTitle: [
@ -232,9 +294,68 @@ export default {
};
},
created() {
this.loadScaleList();
this.getList();
},
methods: {
/** 加载量表列表 */
loadScaleList() {
listScale({ status: '0', pageNum: 1, pageSize: 1000 }).then(response => {
this.scaleList = response.rows || [];
});
},
/** 加载因子列表(用于搜索) */
loadFactorList(scaleId) {
if (!scaleId) {
this.factorList = [];
return;
}
listFactor(scaleId).then(response => {
this.factorList = response.data || [];
}).catch(() => {
this.factorList = [];
});
},
/** 加载因子列表(用于表单) */
loadFormFactorList(scaleId) {
if (!scaleId) {
this.formFactorList = [];
this.form.factorId = undefined;
return;
}
listFactor(scaleId).then(response => {
this.formFactorList = response.data || [];
}).catch(() => {
this.formFactorList = [];
});
},
/** 搜索条件中量表变化 */
handleScaleChange(scaleId) {
this.queryParams.factorId = undefined;
this.loadFactorList(scaleId);
},
/** 表单中量表变化 */
handleFormScaleChange(scaleId) {
this.form.factorId = undefined;
this.loadFormFactorList(scaleId);
},
/** 获取量表名称 */
getScaleName(scaleId) {
if (!scaleId) return null;
const scale = this.scaleList.find(s => s.scaleId === scaleId);
return scale ? scale.scaleName : null;
},
/** 获取因子名称 */
getFactorName(factorId) {
if (!factorId) return null;
//
const factor = this.factorList.find(f => f.factorId === factorId);
if (factor) return factor.factorName;
//
const formFactor = this.formFactorList.find(f => f.factorId === factorId);
if (formFactor) return formFactor.factorName;
return null;
},
/** 查询解释列表 */
getList() {
this.loading = true;
@ -242,6 +363,28 @@ export default {
this.interpretationList = response.rows;
this.total = response.total;
this.loading = false;
//
this.loadAllFactors();
});
},
/** 加载所有解释中涉及的因子 */
loadAllFactors() {
// ID
const scaleIds = [...new Set(this.interpretationList
.map(item => item.scaleId)
.filter(id => id != null))];
//
scaleIds.forEach(scaleId => {
listFactor(scaleId).then(response => {
const factors = response.data || [];
//
factors.forEach(factor => {
if (!this.factorList.find(f => f.factorId === factor.factorId)) {
this.factorList.push(factor);
}
});
}).catch(() => {});
});
},
/** 搜索按钮操作 */
@ -272,6 +415,10 @@ export default {
const interpretationId = row.interpretationId ? row.interpretationId : this.ids[0];
getInterpretation(interpretationId).then(response => {
this.form = response.data;
// ID
if (this.form.scaleId) {
this.loadFormFactorList(this.form.scaleId);
}
this.open = true;
this.title = "修改解释配置";
});

View File

@ -0,0 +1,310 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="量表名称" prop="scaleId">
<el-select v-model="queryParams.scaleId" placeholder="请选择量表" clearable style="width: 200px">
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:value="scale.scaleId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户名称" prop="userId">
<el-select v-model="queryParams.userId" placeholder="请选择用户" clearable filterable style="width: 200px">
<el-option
v-for="user in userList"
:key="user.userId"
:label="user.userName"
:value="user.userId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable style="width: 120px">
<el-option label="有效" value="0" />
<el-option label="无效" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['psychology:permission:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['psychology:permission:remove']">删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="permissionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="权限ID" align="center" prop="permissionId" width="100" />
<el-table-column label="量表名称" align="center" prop="scaleName" width="200" />
<el-table-column label="用户名称" align="center" prop="userName" width="150" />
<el-table-column label="部门名称" align="center" prop="deptName" width="150" />
<el-table-column label="角色名称" align="center" prop="roleName" width="150" />
<el-table-column label="班级名称" align="center" prop="className" width="150" />
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="success">有效</el-tag>
<el-tag v-else type="danger">无效</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['psychology:permission:edit']">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['psychology:permission:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改权限对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="量表" prop="scaleId">
<el-select v-model="form.scaleId" placeholder="请选择量表" style="width: 100%;" filterable>
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:value="scale.scaleId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="用户" prop="userId">
<el-select v-model="form.userId" placeholder="请选择用户(留空表示所有用户)" clearable filterable style="width: 100%;">
<el-option
v-for="user in userList"
:key="user.userId"
:label="user.userName"
:value="user.userId">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="form.startTime"
type="datetime"
placeholder="选择开始时间(留空表示无限制)"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="form.endTime"
type="datetime"
placeholder="选择结束时间(留空表示无限制)"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%;">
</el-date-picker>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">有效</el-radio>
<el-radio label="1">无效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listPermission, getPermission, delPermission, addPermission, updatePermission } from "@/api/psychology/permission";
import { listScale } from "@/api/psychology/scale";
import { listUser } from "@/api/system/user";
export default {
name: "PsyScalePermission",
data() {
return {
//
loading: true,
//
ids: [],
//
single: true,
//
multiple: true,
//
showSearch: true,
//
total: 0,
//
permissionList: [],
//
scaleList: [],
//
userList: [],
//
title: "",
//
open: false,
//
queryParams: {
pageNum: 1,
pageSize: 10,
scaleId: undefined,
userId: undefined,
status: undefined
},
//
form: {},
//
rules: {
scaleId: [
{ required: true, message: "量表不能为空", trigger: "change" }
],
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
]
}
};
},
created() {
this.getList();
this.loadScales();
this.loadUsers();
},
methods: {
/** 查询权限列表 */
getList() {
this.loading = true;
listPermission(this.queryParams).then(response => {
this.permissionList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 加载量表列表 */
loadScales() {
listScale({ status: '0' }).then(response => {
this.scaleList = response.rows || [];
});
},
/** 加载用户列表 */
loadUsers() {
listUser({ status: '0' }).then(response => {
this.userList = response.rows || [];
});
},
//
cancel() {
this.open = false;
this.reset();
},
//
reset() {
this.form = {
permissionId: undefined,
scaleId: undefined,
userId: undefined,
deptId: undefined,
roleId: undefined,
className: undefined,
startTime: undefined,
endTime: undefined,
status: "0",
remark: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.permissionId);
this.single = selection.length != 1;
this.multiple = !selection.length;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加量表权限";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const permissionId = row.permissionId || this.ids[0];
getPermission(permissionId).then(response => {
this.form = response.data;
this.open = true;
this.title = "修改量表权限";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.permissionId != undefined) {
updatePermission(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addPermission(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const permissionIds = row.permissionId || this.ids;
this.$modal.confirm('是否确认删除权限编号为"' + permissionIds + '"的数据项?').then(() => {
return delPermission(permissionIds);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
}
};
</script>

View File

@ -0,0 +1,118 @@
<template>
<div class="app-container">
<el-card shadow="never">
<div slot="header" class="clearfix">
<span>为用户分配量表权限</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleBack">返回</el-button>
</div>
<el-form :model="form" label-width="120px">
<el-form-item label="用户名称">
<el-input v-model="userName" disabled style="width: 300px;" />
</el-form-item>
<el-form-item label="选择量表">
<el-transfer
v-model="selectedScaleIds"
:data="scaleList"
:titles="['可选量表', '已选量表']"
:button-texts="['移除', '添加']"
:format="{
noChecked: '${total}',
hasChecked: '${checked}/${total}'
}"
filterable
filter-placeholder="搜索量表"
style="text-align: left; display: inline-block"
@change="handleChange">
<span slot-scope="{ option }">{{ option.label }} ({{ option.itemCount }})</span>
</el-transfer>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" :loading="loading">保存</el-button>
<el-button @click="handleBack">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { assignUserScales, getUserScaleIds } from "@/api/psychology/permission";
import { listScale } from "@/api/psychology/scale";
import { getUser } from "@/api/system/user";
export default {
name: "UserScalePermission",
data() {
return {
loading: false,
userId: undefined,
userName: "",
scaleList: [],
selectedScaleIds: []
};
},
created() {
const userId = this.$route.params.userId;
if (userId) {
this.userId = parseInt(userId);
this.loadUserInfo();
this.loadScales();
this.loadUserScales();
} else {
this.$modal.msgError("缺少用户ID参数");
this.handleBack();
}
},
methods: {
/** 加载用户信息 */
loadUserInfo() {
getUser(this.userId).then(response => {
this.userName = response.data.userName;
});
},
/** 加载量表列表 */
loadScales() {
listScale({ status: '0' }).then(response => {
this.scaleList = (response.rows || []).map(scale => ({
key: scale.scaleId,
label: scale.scaleName,
itemCount: scale.itemCount || 0
}));
});
},
/** 加载用户已有权限的量表 */
loadUserScales() {
getUserScaleIds(this.userId).then(response => {
this.selectedScaleIds = response.data || [];
});
},
/** 提交表单 */
submitForm() {
this.loading = true;
assignUserScales(this.userId, this.selectedScaleIds).then(response => {
this.$modal.msgSuccess("分配成功");
this.loading = false;
this.handleBack();
}).catch(() => {
this.loading = false;
});
},
/** 返回 */
handleBack() {
this.$router.push("/system/user");
},
/** 选择变化 */
handleChange(value, direction, movedKeys) {
//
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
}
</style>

View File

@ -126,15 +126,67 @@
</el-row>
<el-table :data="ruleList" border>
<el-table-column label="题目ID" align="center" prop="itemId" width="100" />
<el-table-column label="选项ID" align="center" prop="optionIds" width="200" />
<el-table-column label="权重" align="center" prop="weight" width="100" />
<el-table-column label="计算方式" align="center" prop="calculationType" width="120">
<el-table-column label="题目" align="center" width="200">
<template slot-scope="scope">
{{ getCalculationTypeName(scope.row.calculationType) }}
<el-select
v-model="scope.row.itemId"
placeholder="请选择题目"
filterable
style="width: 100%;"
@change="handleRuleItemChange(scope.$index, scope.row.itemId)">
<el-option
v-for="item in itemList"
:key="item.itemId"
:label="item.itemNumber + '. ' + item.itemContent"
:value="item.itemId">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
<el-table-column label="选项" align="center" width="250">
<template slot-scope="scope">
<el-select
v-model="scope.row.optionIdsArray"
placeholder="请选择选项(可选,留空表示所有选项)"
multiple
filterable
clearable
style="width: 100%;"
:disabled="!scope.row.itemId"
@change="handleRuleOptionChange(scope.$index)">
<el-option
v-for="option in getItemOptions(scope.row.itemId)"
:key="option.optionId"
:label="option.optionCode + '. ' + option.optionContent"
:value="option.optionId">
</el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="权重" align="center" width="120">
<template slot-scope="scope">
<el-input-number
v-model="scope.row.weight"
:min="0"
:max="10"
:step="0.1"
:precision="2"
controls-position="right"
style="width: 100%;">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="计算方式" align="center" width="150">
<template slot-scope="scope">
<el-select v-model="scope.row.calculationType" style="width: 100%;">
<el-option label="求和" value="sum" />
<el-option label="平均" value="average" />
<el-option label="最大" value="max" />
<el-option label="最小" value="min" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
<template slot-scope="scope">
<el-button
size="mini"
@ -158,6 +210,7 @@
import { listFactor, getFactor, delFactor, addFactor, updateFactor } from "@/api/psychology/factor";
import { listItem } from "@/api/psychology/item";
import { listFactorRule, saveRules } from "@/api/psychology/factor";
import { listOption } from "@/api/psychology/option";
export default {
name: "ScaleFactor",
@ -175,6 +228,8 @@ export default {
factorList: [],
//
itemList: [],
// key: itemId, value: optionList
optionListCache: {},
//
title: "",
//
@ -311,7 +366,21 @@ export default {
this.ruleOpen = true;
//
listFactorRule(row.factorId).then(response => {
this.ruleList = response.data || [];
const rules = response.data || [];
// optionIds便使
this.ruleList = rules.map(rule => ({
...rule,
optionIdsArray: rule.optionIds ? rule.optionIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) : []
}));
//
const itemIds = [...new Set(rules.map(r => r.itemId).filter(id => id))];
itemIds.forEach(itemId => {
if (!this.optionListCache[itemId]) {
listOption(itemId).then(optionRes => {
this.$set(this.optionListCache, itemId, optionRes.data || []);
});
}
});
});
},
/** 新增规则 */
@ -321,6 +390,7 @@ export default {
factorId: this.currentFactorId,
itemId: null,
optionIds: '',
optionIdsArray: [], //
weight: 1.00,
calculationType: 'sum'
};
@ -330,15 +400,47 @@ export default {
handleDeleteRule(index) {
this.ruleList.splice(index, 1);
},
/** 计算方式名称 */
getCalculationTypeName(type) {
const types = {
sum: '求和',
average: '平均',
max: '最大',
min: '最小'
};
return types[type] || type;
/** 获取题目的选项列表 */
getItemOptions(itemId) {
if (!itemId) {
return [];
}
//
if (this.optionListCache[itemId]) {
return this.optionListCache[itemId];
}
// 访
if (!this.optionListCache[itemId]) {
listOption(itemId).then(response => {
this.$set(this.optionListCache, itemId, response.data || []);
}).catch(() => {
this.$set(this.optionListCache, itemId, []);
});
}
return this.optionListCache[itemId] || [];
},
/** 题目变化时处理 */
handleRuleItemChange(index, itemId) {
const rule = this.ruleList[index];
//
rule.optionIdsArray = [];
rule.optionIds = '';
//
if (itemId) {
listOption(itemId).then(response => {
this.$set(this.optionListCache, itemId, response.data || []);
});
}
},
/** 选项变化时处理 */
handleRuleOptionChange(index) {
const rule = this.ruleList[index];
// ID
if (rule.optionIdsArray && rule.optionIdsArray.length > 0) {
rule.optionIds = rule.optionIdsArray.map(id => String(id)).join(',');
} else {
rule.optionIds = '';
}
},
/** 保存规则 */
submitRules() {
@ -351,7 +453,20 @@ export default {
}
}
saveRules(this.currentFactorId, this.ruleList).then(response => {
// optionIdsArrayoptionIds
const rulesToSave = this.ruleList.map(rule => {
const ruleData = {
ruleId: rule.ruleId,
factorId: rule.factorId,
itemId: rule.itemId,
optionIds: rule.optionIds || (rule.optionIdsArray && rule.optionIdsArray.length > 0 ? rule.optionIdsArray.map(id => String(id)).join(',') : ''),
weight: rule.weight || 1.00,
calculationType: rule.calculationType || 'sum'
};
return ruleData;
});
saveRules(this.currentFactorId, rulesToSave).then(response => {
this.$modal.msgSuccess("保存成功");
this.ruleOpen = false;
});

View File

@ -97,14 +97,19 @@
/>
<el-table-column label="量表类型" align="center" prop="scaleType" width="120">
<template slot-scope="scope">
<dict-tag :options="dict.type.psy_scale_type" :value="scope.row.scaleType"/>
<el-tag v-if="scope.row.scaleType" type="primary">
{{ getDictLabel(dict.type.psy_scale_type, scope.row.scaleType) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="题目数量" align="center" prop="itemCount" width="100" />
<el-table-column label="预计时间(分)" align="center" prop="estimatedTime" width="120" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.psy_scale_status" :value="scope.row.status"/>
<el-tag v-if="scope.row.status === '0'" type="success">正常</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="danger">停用</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="排序" align="center" prop="sortOrder" width="80" />
@ -178,9 +183,9 @@
</el-col>
<el-col :span="12">
<el-form-item label="量表类型" prop="scaleType">
<el-select v-model="form.scaleType" placeholder="请选择量表类型">
<el-select v-model="form.scaleType" placeholder="请选择量表类型" clearable>
<el-option
v-for="dict in dict.type.psy_scale_type"
v-for="dict in uniqueScaleTypes"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -195,7 +200,8 @@
</el-col>
<el-col :span="12">
<el-form-item label="题目数量" prop="itemCount">
<el-input-number v-model="form.itemCount" :min="0" controls-position="right" />
<el-input-number v-model="form.itemCount" :min="0" controls-position="right" :disabled="true" />
<span style="margin-left: 10px; color: #909399; font-size: 12px;">自动计算根据实际题目数量</span>
</el-form-item>
</el-col>
<el-col :span="12">
@ -226,11 +232,8 @@
<el-col :span="24">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.psy_scale_status"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
<el-radio label="0">正常</el-radio>
<el-radio label="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
@ -302,16 +305,35 @@ export default {
scaleType: [
{ required: true, message: "量表类型不能为空", trigger: "change" }
],
itemCount: [
{ required: true, message: "题目数量不能为空", trigger: "blur" }
]
}
}
},
computed: {
/** 去重后的量表类型列表 */
uniqueScaleTypes() {
if (!this.dict.type.psy_scale_type) {
return [];
}
const seen = new Set();
return this.dict.type.psy_scale_type.filter(dict => {
if (seen.has(dict.value)) {
return false;
}
seen.add(dict.value);
return true;
});
}
},
created() {
this.getList()
},
methods: {
/** 获取字典标签 */
getDictLabel(dictList, value) {
if (!dictList || !value) return value;
const dict = dictList.find(item => item.value === value);
return dict ? dict.label : value;
},
/** 查询量表列表 */
getList() {
this.loading = true

View File

@ -0,0 +1,171 @@
<template>
<div class="app-container">
<el-card>
<template #header>
<div class="card-header">
<span>菜单去重工具</span>
</div>
</template>
<div class="menu-cleanup-container">
<div class="button-group">
<el-button type="primary" @click="checkDuplicateMenus" :loading="loading">
<i class="el-icon-search"></i>
检查重复菜单
</el-button>
<el-button type="success" @click="cleanupDuplicateMenus" :loading="loading" :disabled="!hasDuplicates">
<i class="el-icon-delete"></i>
执行菜单去重
</el-button>
<el-button @click="listPsychologyMenus" :loading="loading">
<i class="el-icon-menu"></i>
查看所有菜单
</el-button>
</div>
<el-alert
v-if="message"
:title="message"
:type="messageType"
show-icon
:closable="false"
></el-alert>
<el-table v-loading="loading" :data="tableData" style="width: 100%; margin-top: 20px;">
<el-table-column prop="menu_name" label="菜单名称" width="200"></el-table-column>
<el-table-column prop="path" label="路由路径" width="250"></el-table-column>
<el-table-column prop="component" label="组件路径" width="300"></el-table-column>
<el-table-column prop="parent_id" label="父菜单ID" width="100"></el-table-column>
<el-table-column prop="count" label="重复数量" width="100" v-if="showDuplicates"></el-table-column>
<el-table-column prop="菜单ID列表" label="菜单ID列表" v-if="showDuplicates"></el-table-column>
<el-table-column prop="menu_type" label="类型" width="80" v-if="!showDuplicates"></el-table-column>
<el-table-column prop="visible" label="是否显示" width="100" v-if="!showDuplicates"></el-table-column>
<el-table-column prop="order_num" label="排序" width="80" v-if="!showDuplicates"></el-table-column>
</el-table>
<div v-if="!loading && tableData.length === 0" class="empty-state">
<el-empty description="暂无数据"></el-empty>
</div>
</div>
</el-card>
</div>
</template>
<script>
import { axios } from '@/utils/request'
export default {
name: 'MenuCleanup',
data() {
return {
loading: false,
tableData: [],
message: '',
messageType: 'info',
hasDuplicates: false,
showDuplicates: true
}
},
methods: {
//
checkDuplicateMenus() {
this.loading = true
this.showDuplicates = true
axios.post('/system/menu/cleanup/check').then(res => {
if (res.code === 200) {
this.tableData = res.data
this.hasDuplicates = this.tableData.length > 0
if (this.hasDuplicates) {
this.message = `发现 ${this.tableData.length} 组重复菜单!`
this.messageType = 'warning'
} else {
this.message = '未发现重复菜单!'
this.messageType = 'success'
}
} else {
this.message = res.msg || '检查失败'
this.messageType = 'error'
this.hasDuplicates = false
}
}).catch(err => {
this.message = '请求失败:' + err
this.messageType = 'error'
this.hasDuplicates = false
}).finally(() => {
this.loading = false
})
},
//
cleanupDuplicateMenus() {
this.$confirm('确定要清理所有重复菜单吗?清理操作不可撤销!', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.loading = true
axios.post('/system/menu/cleanup/cleanup').then(res => {
if (res.code === 200) {
this.message = res.msg || '清理成功'
this.messageType = 'success'
this.checkDuplicateMenus() //
} else {
this.message = res.msg || '清理失败'
this.messageType = 'error'
}
}).catch(err => {
this.message = '请求失败:' + err
this.messageType = 'error'
}).finally(() => {
this.loading = false
})
})
},
//
listPsychologyMenus() {
this.loading = true
this.showDuplicates = false
axios.post('/system/menu/cleanup/list').then(res => {
if (res.code === 200) {
this.tableData = res.data
this.message = `共查询到 ${this.tableData.length} 个心理学相关菜单`
this.messageType = 'info'
} else {
this.message = res.msg || '查询失败'
this.messageType = 'error'
}
}).catch(err => {
this.message = '请求失败:' + err
this.messageType = 'error'
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-cleanup-container {
padding: 10px;
}
.button-group {
margin-bottom: 20px;
}
.button-group .el-button {
margin-right: 10px;
}
.empty-state {
margin-top: 50px;
}
</style>

View File

@ -82,6 +82,7 @@
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleResetPwd" icon="el-icon-key" v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item>
<el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check" v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item>
<el-dropdown-item command="handleAuthScale" icon="el-icon-document" v-hasPermi="['psychology:permission:edit']">分配量表权限</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@ -432,6 +433,9 @@ export default {
case "handleAuthRole":
this.handleAuthRole(row)
break
case "handleAuthScale":
this.handleAuthScale(row)
break
default:
break
}
@ -486,6 +490,10 @@ export default {
const userId = row.userId
this.$router.push("/system/user-auth/role/" + userId)
},
/** 分配量表权限操作 */
handleAuthScale: function(row) {
this.$router.push("/psychology/permission/user/" + row.userId)
},
/** 提交按钮 */
submitForm: function() {
this.$refs["form"].validate(valid => {

View File

@ -0,0 +1,156 @@
package com.ddnai.admin.controller;
import com.ddnai.common.core.controller.BaseController;
import com.ddnai.common.core.domain.AjaxResult;
import com.ddnai.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 菜单去重清理控制器
* 用于检测和清理系统中重复的菜单
*/
@RestController
@RequestMapping("/system/menu/cleanup")
public class MenuCleanupController extends BaseController
{
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 检查重复菜单
*/
@PostMapping("/check")
public AjaxResult checkDuplicateMenus()
{
// 查找所有重复的菜单基于menu_name和path组合
String sql = "SELECT " +
" menu_name AS '菜单名称'," +
" path AS '路由路径'," +
" component AS '组件路径'," +
" parent_id AS '父菜单ID'," +
" COUNT(*) AS '重复数量'," +
" GROUP_CONCAT(menu_id ORDER BY menu_id SEPARATOR ', ') AS '菜单ID列表' " +
"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 '%预警%' " +
"GROUP BY menu_name, path, component, parent_id " +
"HAVING COUNT(*) > 1 " +
"ORDER BY COUNT(*) DESC, menu_name";
List<Map<String, Object>> duplicates = jdbcTemplate.queryForList(sql);
return AjaxResult.success(duplicates);
}
/**
* 执行菜单去重
*/
@PostMapping("/cleanup")
public AjaxResult cleanupDuplicateMenus()
{
try
{
// 删除"心理测评管理"目录的重复项保留第一个
int deleteCount1 = jdbcTemplate.update(
"DELETE t1 FROM sys_menu t1 " +
"INNER JOIN sys_menu t2 " +
"WHERE t1.menu_name = '心理测评管理' " +
" AND t1.parent_id = 0 " +
" AND t2.menu_name = '心理测评管理' " +
" AND t2.parent_id = 0 " +
" AND t1.menu_id > t2.menu_id"
);
// 删除"心理网站管理"目录的重复项
int deleteCount2 = jdbcTemplate.update(
"DELETE t1 FROM sys_menu t1 " +
"INNER JOIN sys_menu t2 " +
"WHERE t1.menu_name = '心理网站管理' " +
" AND t1.parent_id = 0 " +
" AND t2.menu_name = '心理网站管理' " +
" AND t2.parent_id = 0 " +
" AND t1.menu_id > t2.menu_id"
);
// 删除其他重复菜单基于path和component
int deleteCount3 = jdbcTemplate.update(
"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 " +
" 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 '%档案%')"
);
int totalDeleted = deleteCount1 + deleteCount2 + deleteCount3;
return AjaxResult.success("菜单去重完成,共删除 " + totalDeleted + " 个重复菜单");
}
catch (Exception e)
{
return AjaxResult.error("菜单去重失败:" + e.getMessage());
}
}
/**
* 列出所有心理学相关菜单按层级
*/
@PostMapping("/list")
public AjaxResult listPsychologyMenus()
{
String sql = "SELECT " +
" menu_id, " +
" menu_name AS '菜单名称', " +
" parent_id AS '父菜单ID', " +
" path AS '路由路径', " +
" component AS '组件路径', " +
" menu_type AS '类型', " +
" visible AS '是否显示', " +
" order_num 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 '%预警%' " +
"ORDER BY parent_id, order_num, menu_id";
List<Map<String, Object>> menus = jdbcTemplate.queryForList(sql);
return AjaxResult.success(menus);
}
}

View File

@ -0,0 +1,127 @@
package com.ddnai.web.controller.psychology;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ddnai.common.annotation.Log;
import com.ddnai.common.core.controller.BaseController;
import com.ddnai.common.core.domain.AjaxResult;
import com.ddnai.common.core.page.TableDataInfo;
import com.ddnai.common.enums.BusinessType;
import com.ddnai.system.domain.psychology.PsyScalePermission;
import com.ddnai.system.service.psychology.IPsyScalePermissionService;
/**
* 量表权限 信息操作处理
*
* @author ddnai
*/
@RestController
@RequestMapping("/psychology/permission")
public class PsyScalePermissionController extends BaseController
{
@Autowired
private IPsyScalePermissionService permissionService;
/**
* 获取权限列表
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:list')")
@GetMapping("/list")
public TableDataInfo list(PsyScalePermission permission)
{
startPage();
List<PsyScalePermission> list = permissionService.selectPermissionList(permission);
return getDataTable(list);
}
/**
* 根据权限ID获取详细信息
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:query')")
@GetMapping(value = "/{permissionId}")
public AjaxResult getInfo(@PathVariable Long permissionId)
{
return success(permissionService.selectPermissionById(permissionId));
}
/**
* 根据用户ID获取用户有权限访问的量表ID列表
*/
@GetMapping("/user/{userId}/scales")
public AjaxResult getUserScaleIds(@PathVariable Long userId)
{
List<Long> scaleIds = permissionService.selectScaleIdsByUserId(userId);
return success(scaleIds);
}
/**
* 检查用户是否有权限访问指定量表
*/
@GetMapping("/check/{userId}/{scaleId}")
public AjaxResult checkPermission(@PathVariable Long userId, @PathVariable Long scaleId)
{
boolean hasPermission = permissionService.checkUserScalePermission(userId, scaleId);
return success(hasPermission);
}
/**
* 新增权限
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:add')")
@Log(title = "量表权限", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@Validated @RequestBody PsyScalePermission permission)
{
permission.setCreateBy(getUsername());
return toAjax(permissionService.insertPermission(permission));
}
/**
* 修改权限
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:edit')")
@Log(title = "量表权限", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody PsyScalePermission permission)
{
permission.setUpdateBy(getUsername());
return toAjax(permissionService.updatePermission(permission));
}
/**
* 删除权限
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:remove')")
@Log(title = "量表权限", businessType = BusinessType.DELETE)
@DeleteMapping("/{permissionIds}")
public AjaxResult remove(@PathVariable Long[] permissionIds)
{
return toAjax(permissionService.deletePermissionByIds(permissionIds));
}
/**
* 批量分配用户量表权限
*/
@PreAuthorize("@ss.hasPermi('psychology:permission:edit')")
@Log(title = "量表权限", businessType = BusinessType.UPDATE)
@PostMapping("/assign")
public AjaxResult assignUserScales(@RequestBody java.util.Map<String, Object> params)
{
Long userId = Long.valueOf(params.get("userId").toString());
@SuppressWarnings("unchecked")
List<Long> scaleIdList = (List<Long>) params.get("scaleIds");
Long[] scaleIds = scaleIdList != null ? scaleIdList.toArray(new Long[0]) : new Long[0];
return toAjax(permissionService.batchAssignUserScalePermission(userId, scaleIds));
}
}

View File

@ -0,0 +1,65 @@
package com.ddnai.admin.test;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.ddnai.common.core.domain.model.LoginUser;
import com.ddnai.common.core.domain.entity.SysUser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashSet;
import java.util.Set;
/**
* Fastjson序列化测试类
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class FastJsonTest {
@Test
public void testLoginUserSerialization() {
// 创建一个简单的LoginUser对象
LoginUser loginUser = new LoginUser();
loginUser.setUsername("admin");
loginUser.setUserId(1L);
loginUser.setBrowser("Chrome");
loginUser.setOs("Windows");
loginUser.setIpaddr("127.0.0.1");
// 设置权限
Set<String> permissions = new HashSet<>();
permissions.add("*:*:*");
loginUser.setPermissions(permissions);
// 设置SysUser对象
SysUser sysUser = new SysUser();
sysUser.setUserId(1L);
sysUser.setUserName("admin");
sysUser.setNickName("管理员");
sysUser.setStatus("0");
loginUser.setUser(sysUser);
System.out.println("=== 开始序列化测试 ===");
try {
// 序列化
String json = JSON.toJSONString(loginUser, JSONWriter.Feature.WriteClassName);
System.out.println("序列化结果: " + json);
// 反序列化 - 直接指定类型
LoginUser deserializedUser = JSON.parseObject(json, LoginUser.class, JSONReader.Feature.SupportAutoType);
System.out.println("反序列化成功! 用户名: " + deserializedUser.getUsername());
System.out.println("反序列化成功! 嵌套用户ID: " + deserializedUser.getUser().getUserId());
} catch (Exception e) {
System.out.println("序列化/反序列化异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("=== 测试结束 ===");
}
}

View File

@ -0,0 +1,56 @@
package com.ddnai.admin.test;
import com.alibaba.fastjson2.JSON;
import com.ddnai.common.core.domain.entity.SysUser;
import com.ddnai.common.core.domain.model.LoginUser;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
/**
* 测试LoginUser类的序列化和反序列化
*/
public class LoginUserSerializationTest {
@Test
public void testSerializationAndDeserialization() {
// 创建测试对象
SysUser user = new SysUser();
user.setUserId(1L);
user.setUserName("admin");
user.setNickName("管理员");
Set<String> permissions = new HashSet<>();
permissions.add("*:*:*");
LoginUser loginUser = new LoginUser(user, permissions);
loginUser.setUserId(1L);
loginUser.setDeptId(103L);
loginUser.setToken("test-token");
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(System.currentTimeMillis() + 3600000);
loginUser.setIpaddr("127.0.0.1");
loginUser.setLoginLocation("内网IP");
loginUser.setBrowser("Chrome");
loginUser.setOs("Windows 10");
// 序列化
String jsonString = JSON.toJSONString(loginUser, JSONWriter.Feature.WriteClassName);
System.out.println("序列化结果:");
System.out.println(jsonString);
// 反序列化
try {
LoginUser deserializedUser = JSON.parseObject(jsonString, LoginUser.class);
System.out.println("\n反序列化结果");
System.out.println("用户名: " + deserializedUser.getUsername());
System.out.println("用户ID: " + deserializedUser.getUserId());
System.out.println("部门ID: " + deserializedUser.getDeptId());
System.out.println("Token: " + deserializedUser.getToken());
System.out.println("\n反序列化成功");
} catch (Exception e) {
System.err.println("\n反序列化失败");
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,220 @@
package com.ddnai.admin.tool;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 菜单去重工具
* 用于检测和清理数据库中重复的菜单
* 可以直接运行此类来执行菜单去重
*/
public class MenuCleanupTool {
// 数据库连接信息
private static final String URL = "jdbc:mysql://localhost:3306/ry_news?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
private static final String USERNAME = "root";
private static final String PASSWORD = "password"; // 请根据实际情况修改密码
public static void main(String[] args) {
Connection conn = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立数据库连接
System.out.println("正在连接数据库...");
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
System.out.println("数据库连接成功!");
// 1. 先检查重复菜单
System.out.println("\n===============================================");
System.out.println("开始检查重复菜单...");
List<DuplicateMenuInfo> duplicates = checkDuplicateMenus(conn);
if (duplicates.isEmpty()) {
System.out.println("未发现重复菜单!");
} else {
System.out.println("发现 " + duplicates.size() + " 组重复菜单:");
for (DuplicateMenuInfo info : duplicates) {
System.out.println("- 菜单名称: " + info.getMenuName() + ", 重复数量: " + info.getCount() + ", 菜单ID: " + info.getMenuIds());
}
// 2. 执行去重操作
System.out.println("\n===============================================");
System.out.println("开始执行菜单去重...");
int totalDeleted = cleanupDuplicateMenus(conn);
System.out.println("\n菜单去重完成共删除 " + totalDeleted + " 个重复菜单。");
// 3. 验证去重结果
System.out.println("\n===============================================");
System.out.println("验证去重结果...");
List<DuplicateMenuInfo> afterCleanup = checkDuplicateMenus(conn);
if (afterCleanup.isEmpty()) {
System.out.println("验证成功!所有重复菜单已被清理。");
} else {
System.out.println("仍有 " + afterCleanup.size() + " 组重复菜单未被清理,请检查。");
}
}
} catch (Exception e) {
System.err.println("执行菜单去重时出错:" + e.getMessage());
e.printStackTrace();
} finally {
// 关闭数据库连接
if (conn != null) {
try {
conn.close();
System.out.println("\n数据库连接已关闭。");
} catch (SQLException e) {
System.err.println("关闭数据库连接时出错:" + e.getMessage());
}
}
}
}
/**
* 检查重复菜单
*/
private static List<DuplicateMenuInfo> checkDuplicateMenus(Connection conn) throws SQLException {
List<DuplicateMenuInfo> duplicates = new ArrayList<>();
String sql = "SELECT " +
" menu_name, " +
" COUNT(*) AS count, " +
" GROUP_CONCAT(menu_id ORDER BY menu_id SEPARATOR ', ') AS menu_ids " +
"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 '%预警%' " +
"GROUP BY menu_name, path, component, parent_id " +
"HAVING COUNT(*) > 1 " +
"ORDER BY COUNT(*) DESC, menu_name";
try (PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
DuplicateMenuInfo info = new DuplicateMenuInfo();
info.setMenuName(rs.getString("menu_name"));
info.setCount(rs.getInt("count"));
info.setMenuIds(rs.getString("menu_ids"));
duplicates.add(info);
}
}
return duplicates;
}
/**
* 执行菜单去重
*/
private static int cleanupDuplicateMenus(Connection conn) throws SQLException {
int totalDeleted = 0;
// 1. 删除"心理测评管理"目录的重复项保留第一个
String sql1 = "DELETE t1 FROM sys_menu t1 " +
"INNER JOIN sys_menu t2 " +
"WHERE t1.menu_name = '心理测评管理' " +
" AND t1.parent_id = 0 " +
" AND t2.menu_name = '心理测评管理' " +
" AND t2.parent_id = 0 " +
" AND t1.menu_id > t2.menu_id";
try (PreparedStatement stmt = conn.prepareStatement(sql1)) {
int deleted1 = stmt.executeUpdate();
totalDeleted += deleted1;
System.out.println("删除重复的'心理测评管理'目录: " + deleted1 + "");
}
// 2. 删除"心理网站管理"目录的重复项
String sql2 = "DELETE t1 FROM sys_menu t1 " +
"INNER JOIN sys_menu t2 " +
"WHERE t1.menu_name = '心理网站管理' " +
" AND t1.parent_id = 0 " +
" AND t2.menu_name = '心理网站管理' " +
" AND t2.parent_id = 0 " +
" AND t1.menu_id > t2.menu_id";
try (PreparedStatement stmt = conn.prepareStatement(sql2)) {
int deleted2 = stmt.executeUpdate();
totalDeleted += deleted2;
System.out.println("删除重复的'心理网站管理'目录: " + deleted2 + "");
}
// 3. 删除其他重复菜单基于path和component
String sql3 = "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 " +
" 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 '%档案%')";
try (PreparedStatement stmt = conn.prepareStatement(sql3)) {
int deleted3 = stmt.executeUpdate();
totalDeleted += deleted3;
System.out.println("删除其他重复菜单: " + deleted3 + "");
}
return totalDeleted;
}
/**
* 重复菜单信息类
*/
static class DuplicateMenuInfo {
private String menuName;
private int count;
private String menuIds;
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public String getMenuIds() {
return menuIds;
}
public void setMenuIds(String menuIds) {
this.menuIds = menuIds;
}
}
}

View File

@ -0,0 +1,208 @@
package com.ddnai.system.domain.psychology;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ddnai.common.core.domain.BaseEntity;
/**
* 量表权限表 psy_scale_permission
*
* @author ddnai
*/
public class PsyScalePermission extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 权限ID */
private Long permissionId;
/** 量表ID */
private Long scaleId;
/** 部门ID关联sys_dept为空表示所有部门 */
private Long deptId;
/** 角色ID关联sys_role为空表示所有角色 */
private Long roleId;
/** 用户ID关联sys_user为空表示所有用户 */
private Long userId;
/** 班级名称(特殊字段,用于罪犯管理) */
private String className;
/** 开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private java.util.Date startTime;
/** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private java.util.Date endTime;
/** 状态0有效 1无效 */
private String status;
/** 量表名称(关联查询字段,不存储在表中) */
private String scaleName;
/** 部门名称(关联查询字段,不存储在表中) */
private String deptName;
/** 角色名称(关联查询字段,不存储在表中) */
private String roleName;
/** 用户名称(关联查询字段,不存储在表中) */
private String userName;
public Long getPermissionId()
{
return permissionId;
}
public void setPermissionId(Long permissionId)
{
this.permissionId = permissionId;
}
public Long getScaleId()
{
return scaleId;
}
public void setScaleId(Long scaleId)
{
this.scaleId = scaleId;
}
public Long getDeptId()
{
return deptId;
}
public void setDeptId(Long deptId)
{
this.deptId = deptId;
}
public Long getRoleId()
{
return roleId;
}
public void setRoleId(Long roleId)
{
this.roleId = roleId;
}
public Long getUserId()
{
return userId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public String getClassName()
{
return className;
}
public void setClassName(String className)
{
this.className = className;
}
public java.util.Date getStartTime()
{
return startTime;
}
public void setStartTime(java.util.Date startTime)
{
this.startTime = startTime;
}
public java.util.Date getEndTime()
{
return endTime;
}
public void setEndTime(java.util.Date endTime)
{
this.endTime = endTime;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getScaleName()
{
return scaleName;
}
public void setScaleName(String scaleName)
{
this.scaleName = scaleName;
}
public String getDeptName()
{
return deptName;
}
public void setDeptName(String deptName)
{
this.deptName = deptName;
}
public String getRoleName()
{
return roleName;
}
public void setRoleName(String roleName)
{
this.roleName = roleName;
}
public String getUserName()
{
return userName;
}
public void setUserName(String userName)
{
this.userName = userName;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("permissionId", getPermissionId())
.append("scaleId", getScaleId())
.append("deptId", getDeptId())
.append("roleId", getRoleId())
.append("userId", getUserId())
.append("className", getClassName())
.append("startTime", getStartTime())
.append("endTime", getEndTime())
.append("status", getStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.toString();
}
}

View File

@ -0,0 +1,95 @@
package com.ddnai.system.mapper.psychology;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ddnai.system.domain.psychology.PsyScalePermission;
/**
* 量表权限表 数据层
*
* @author ddnai
*/
public interface PsyScalePermissionMapper
{
/**
* 查询量表权限信息
*
* @param permissionId 权限ID
* @return 权限信息
*/
public PsyScalePermission selectPermissionById(Long permissionId);
/**
* 查询量表权限列表
*
* @param permission 权限信息
* @return 权限集合
*/
public List<PsyScalePermission> selectPermissionList(PsyScalePermission permission);
/**
* 根据用户ID查询用户有权限访问的量表ID列表
*
* @param userId 用户ID
* @return 量表ID列表
*/
public List<Long> selectScaleIdsByUserId(@Param("userId") Long userId);
/**
* 根据用户ID和量表ID检查用户是否有权限
*
* @param userId 用户ID
* @param scaleId 量表ID
* @return 权限数量>0表示有权限
*/
public int checkUserScalePermission(@Param("userId") Long userId, @Param("scaleId") Long scaleId);
/**
* 新增量表权限
*
* @param permission 权限信息
* @return 结果
*/
public int insertPermission(PsyScalePermission permission);
/**
* 修改量表权限
*
* @param permission 权限信息
* @return 结果
*/
public int updatePermission(PsyScalePermission permission);
/**
* 删除量表权限
*
* @param permissionId 权限ID
* @return 结果
*/
public int deletePermissionById(Long permissionId);
/**
* 批量删除量表权限
*
* @param permissionIds 需要删除的权限ID
* @return 结果
*/
public int deletePermissionByIds(Long[] permissionIds);
/**
* 根据用户ID删除该用户的所有权限
*
* @param userId 用户ID
* @return 结果
*/
public int deletePermissionByUserId(Long userId);
/**
* 根据量表ID删除该量表的所有权限
*
* @param scaleId 量表ID
* @return 结果
*/
public int deletePermissionByScaleId(Long scaleId);
}

View File

@ -0,0 +1,174 @@
package com.ddnai.system.service.impl.psychology;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ddnai.common.utils.SecurityUtils;
import com.ddnai.system.domain.psychology.PsyScalePermission;
import com.ddnai.system.mapper.psychology.PsyScalePermissionMapper;
import com.ddnai.system.service.psychology.IPsyScalePermissionService;
/**
* 量表权限 服务层实现
*
* @author ddnai
*/
@Service
public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
{
@Autowired
private PsyScalePermissionMapper permissionMapper;
/**
* 查询量表权限信息
*
* @param permissionId 权限ID
* @return 权限信息
*/
@Override
public PsyScalePermission selectPermissionById(Long permissionId)
{
return permissionMapper.selectPermissionById(permissionId);
}
/**
* 查询量表权限列表
*
* @param permission 权限信息
* @return 权限集合
*/
@Override
public List<PsyScalePermission> selectPermissionList(PsyScalePermission permission)
{
return permissionMapper.selectPermissionList(permission);
}
/**
* 根据用户ID查询用户有权限访问的量表ID列表
*
* @param userId 用户ID
* @return 量表ID列表
*/
@Override
public List<Long> selectScaleIdsByUserId(Long userId)
{
return permissionMapper.selectScaleIdsByUserId(userId);
}
/**
* 检查用户是否有权限访问指定量表
*
* @param userId 用户ID
* @param scaleId 量表ID
* @return 是否有权限
*/
@Override
public boolean checkUserScalePermission(Long userId, Long scaleId)
{
// 管理员拥有所有权限
if (userId != null && userId == 1L)
{
return true;
}
int count = permissionMapper.checkUserScalePermission(userId, scaleId);
return count > 0;
}
/**
* 新增量表权限
*
* @param permission 权限信息
* @return 结果
*/
@Override
public int insertPermission(PsyScalePermission permission)
{
if (permission.getStatus() == null)
{
permission.setStatus("0");
}
return permissionMapper.insertPermission(permission);
}
/**
* 修改量表权限
*
* @param permission 权限信息
* @return 结果
*/
@Override
public int updatePermission(PsyScalePermission permission)
{
return permissionMapper.updatePermission(permission);
}
/**
* 批量删除量表权限
*
* @param permissionIds 需要删除的权限ID
* @return 结果
*/
@Override
public int deletePermissionByIds(Long[] permissionIds)
{
return permissionMapper.deletePermissionByIds(permissionIds);
}
/**
* 根据用户ID删除该用户的所有权限
*
* @param userId 用户ID
* @return 结果
*/
@Override
public int deletePermissionByUserId(Long userId)
{
return permissionMapper.deletePermissionByUserId(userId);
}
/**
* 根据量表ID删除该量表的所有权限
*
* @param scaleId 量表ID
* @return 结果
*/
@Override
public int deletePermissionByScaleId(Long scaleId)
{
return permissionMapper.deletePermissionByScaleId(scaleId);
}
/**
* 批量分配用户量表权限
*
* @param userId 用户ID
* @param scaleIds 量表ID数组
* @return 结果
*/
@Override
@Transactional
public int batchAssignUserScalePermission(Long userId, Long[] scaleIds)
{
// 先删除该用户的所有现有权限
permissionMapper.deletePermissionByUserId(userId);
// 批量插入新权限
int count = 0;
if (scaleIds != null && scaleIds.length > 0)
{
String username = SecurityUtils.getUsername();
for (Long scaleId : scaleIds)
{
PsyScalePermission permission = new PsyScalePermission();
permission.setUserId(userId);
permission.setScaleId(scaleId);
permission.setStatus("0");
permission.setCreateBy(username);
count += permissionMapper.insertPermission(permission);
}
}
return count;
}
}

View File

@ -0,0 +1,95 @@
package com.ddnai.system.service.psychology;
import java.util.List;
import com.ddnai.system.domain.psychology.PsyScalePermission;
/**
* 量表权限 服务层
*
* @author ddnai
*/
public interface IPsyScalePermissionService
{
/**
* 查询量表权限信息
*
* @param permissionId 权限ID
* @return 权限信息
*/
public PsyScalePermission selectPermissionById(Long permissionId);
/**
* 查询量表权限列表
*
* @param permission 权限信息
* @return 权限集合
*/
public List<PsyScalePermission> selectPermissionList(PsyScalePermission permission);
/**
* 根据用户ID查询用户有权限访问的量表ID列表
*
* @param userId 用户ID
* @return 量表ID列表
*/
public List<Long> selectScaleIdsByUserId(Long userId);
/**
* 检查用户是否有权限访问指定量表
*
* @param userId 用户ID
* @param scaleId 量表ID
* @return 是否有权限
*/
public boolean checkUserScalePermission(Long userId, Long scaleId);
/**
* 新增量表权限
*
* @param permission 权限信息
* @return 结果
*/
public int insertPermission(PsyScalePermission permission);
/**
* 修改量表权限
*
* @param permission 权限信息
* @return 结果
*/
public int updatePermission(PsyScalePermission permission);
/**
* 批量删除量表权限
*
* @param permissionIds 需要删除的权限ID
* @return 结果
*/
public int deletePermissionByIds(Long[] permissionIds);
/**
* 根据用户ID删除该用户的所有权限
*
* @param userId 用户ID
* @return 结果
*/
public int deletePermissionByUserId(Long userId);
/**
* 根据量表ID删除该量表的所有权限
*
* @param scaleId 量表ID
* @return 结果
*/
public int deletePermissionByScaleId(Long scaleId);
/**
* 批量分配用户量表权限
*
* @param userId 用户ID
* @param scaleIds 量表ID数组
* @return 结果
*/
public int batchAssignUserScalePermission(Long userId, Long[] scaleIds);
}

View File

@ -29,40 +29,46 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectScaleVo">
select scale_id, scale_code, scale_name, scale_en_name, scale_type, scale_version,
scale_intro, scale_description, item_count, estimated_time, target_population,
author, source, reference, status, sort_order, create_by, create_time,
update_by, update_time, remark
from psy_scale
select s.scale_id, s.scale_code, s.scale_name, s.scale_en_name, s.scale_type, s.scale_version,
s.scale_intro, s.scale_description,
COALESCE(COUNT(i.item_id), 0) as item_count,
s.estimated_time, s.target_population,
s.author, s.source, s.reference, s.status, s.sort_order, s.create_by, s.create_time,
s.update_by, s.update_time, s.remark
from psy_scale s
left join psy_scale_item i on s.scale_id = i.scale_id
</sql>
<select id="selectScaleById" parameterType="Long" resultMap="PsyScaleResult">
<include refid="selectScaleVo"/>
where scale_id = #{scaleId}
where s.scale_id = #{scaleId}
group by s.scale_id
</select>
<select id="selectScaleByCode" parameterType="String" resultMap="PsyScaleResult">
<include refid="selectScaleVo"/>
where scale_code = #{scaleCode}
where s.scale_code = #{scaleCode}
group by s.scale_id
</select>
<select id="selectScaleList" parameterType="com.ddnai.system.domain.psychology.PsyScale" resultMap="PsyScaleResult">
<include refid="selectScaleVo"/>
<where>
<if test="scaleName != null and scaleName != ''">
AND scale_name like concat('%', #{scaleName}, '%')
AND s.scale_name like concat('%', #{scaleName}, '%')
</if>
<if test="scaleCode != null and scaleCode != ''">
AND scale_code like concat('%', #{scaleCode}, '%')
AND s.scale_code like concat('%', #{scaleCode}, '%')
</if>
<if test="scaleType != null and scaleType != ''">
AND scale_type = #{scaleType}
AND s.scale_type = #{scaleType}
</if>
<if test="status != null and status != ''">
AND status = #{status}
AND s.status = #{status}
</if>
</where>
order by sort_order, create_time desc
group by s.scale_id
order by s.sort_order, s.create_time desc
</select>
<insert id="insertScale" parameterType="com.ddnai.system.domain.psychology.PsyScale">

View File

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ddnai.system.mapper.psychology.PsyScalePermissionMapper">
<resultMap type="com.ddnai.system.domain.psychology.PsyScalePermission" id="PsyScalePermissionResult">
<result property="permissionId" column="permission_id" />
<result property="scaleId" column="scale_id" />
<result property="deptId" column="dept_id" />
<result property="roleId" column="role_id" />
<result property="userId" column="user_id" />
<result property="className" column="class_name" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="status" column="status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="scaleName" column="scale_name" />
<result property="deptName" column="dept_name" />
<result property="roleName" column="role_name" />
<result property="userName" column="user_name" />
</resultMap>
<sql id="selectPermissionVo">
select p.permission_id, p.scale_id, p.dept_id, p.role_id, p.user_id, p.class_name,
p.start_time, p.end_time, p.status, p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
s.scale_name, d.dept_name, r.role_name, u.user_name
from psy_scale_permission p
left join psy_scale s on p.scale_id = s.scale_id
left join sys_dept d on p.dept_id = d.dept_id
left join sys_role r on p.role_id = r.role_id
left join sys_user u on p.user_id = u.user_id
</sql>
<select id="selectPermissionById" parameterType="Long" resultMap="PsyScalePermissionResult">
<include refid="selectPermissionVo"/>
where p.permission_id = #{permissionId}
</select>
<select id="selectPermissionList" parameterType="com.ddnai.system.domain.psychology.PsyScalePermission" resultMap="PsyScalePermissionResult">
<include refid="selectPermissionVo"/>
<where>
<if test="scaleId != null">
AND p.scale_id = #{scaleId}
</if>
<if test="deptId != null">
AND p.dept_id = #{deptId}
</if>
<if test="roleId != null">
AND p.role_id = #{roleId}
</if>
<if test="userId != null">
AND p.user_id = #{userId}
</if>
<if test="status != null and status != ''">
AND p.status = #{status}
</if>
</where>
order by p.create_time desc
</select>
<select id="selectScaleIdsByUserId" parameterType="Long" resultType="Long">
select distinct p.scale_id
from psy_scale_permission p
where p.status = '0'
and (
-- 用户直接权限
(p.user_id = #{userId})
-- 或者角色权限(需要关联用户角色表)
or (p.role_id is not null and p.role_id in (
select role_id from sys_user_role where user_id = #{userId}
))
-- 或者部门权限(需要关联用户部门)
or (p.dept_id is not null and p.dept_id in (
select dept_id from sys_user where user_id = #{userId}
))
-- 或者所有用户权限user_id, role_id, dept_id 都为空)
or (p.user_id is null and p.role_id is null and p.dept_id is null)
)
-- 检查时间范围
and (p.start_time is null or p.start_time &lt;= now())
and (p.end_time is null or p.end_time >= now())
</select>
<select id="checkUserScalePermission" resultType="int">
select count(1)
from psy_scale_permission p
where p.scale_id = #{scaleId}
and p.status = '0'
and (
-- 用户直接权限
(p.user_id = #{userId})
-- 或者角色权限
or (p.role_id is not null and p.role_id in (
select role_id from sys_user_role where user_id = #{userId}
))
-- 或者部门权限
or (p.dept_id is not null and p.dept_id in (
select dept_id from sys_user where user_id = #{userId}
))
-- 或者所有用户权限
or (p.user_id is null and p.role_id is null and p.dept_id is null)
)
-- 检查时间范围
and (p.start_time is null or p.start_time &lt;= now())
and (p.end_time is null or p.end_time >= now())
</select>
<insert id="insertPermission" parameterType="com.ddnai.system.domain.psychology.PsyScalePermission" useGeneratedKeys="true" keyProperty="permissionId">
insert into psy_scale_permission (
<if test="scaleId != null">scale_id, </if>
<if test="deptId != null">dept_id, </if>
<if test="roleId != null">role_id, </if>
<if test="userId != null">user_id, </if>
<if test="className != null and className != ''">class_name, </if>
<if test="startTime != null">start_time, </if>
<if test="endTime != null">end_time, </if>
<if test="status != null and status != ''">status, </if>
<if test="remark != null">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="scaleId != null">#{scaleId}, </if>
<if test="deptId != null">#{deptId}, </if>
<if test="roleId != null">#{roleId}, </if>
<if test="userId != null">#{userId}, </if>
<if test="className != null and className != ''">#{className}, </if>
<if test="startTime != null">#{startTime}, </if>
<if test="endTime != null">#{endTime}, </if>
<if test="status != null and status != ''">#{status}, </if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<update id="updatePermission" parameterType="com.ddnai.system.domain.psychology.PsyScalePermission">
update psy_scale_permission
<set>
<if test="scaleId != null">scale_id = #{scaleId}, </if>
<if test="deptId != null">dept_id = #{deptId}, </if>
<if test="roleId != null">role_id = #{roleId}, </if>
<if test="userId != null">user_id = #{userId}, </if>
<if test="className != null and className != ''">class_name = #{className}, </if>
<if test="startTime != null">start_time = #{startTime}, </if>
<if test="endTime != null">end_time = #{endTime}, </if>
<if test="status != null and status != ''">status = #{status}, </if>
<if test="remark != null">remark = #{remark}, </if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
update_time = sysdate()
</set>
where permission_id = #{permissionId}
</update>
<delete id="deletePermissionById" parameterType="Long">
delete from psy_scale_permission where permission_id = #{permissionId}
</delete>
<delete id="deletePermissionByIds" parameterType="String">
delete from psy_scale_permission where permission_id in
<foreach item="permissionId" collection="array" open="(" separator="," close=")">
#{permissionId}
</foreach>
</delete>
<delete id="deletePermissionByUserId" parameterType="Long">
delete from psy_scale_permission where user_id = #{userId}
</delete>
<delete id="deletePermissionByScaleId" parameterType="Long">
delete from psy_scale_permission where scale_id = #{scaleId}
</delete>
</mapper>

View File

@ -0,0 +1,141 @@
-- ========================================
-- 增强版清理重复菜单SQL脚本
-- 用途:彻底删除数据库中重复的心理学相关菜单
-- 注意:执行前请备份数据库!
-- ========================================
USE ry_news;
SET NAMES utf8mb4;
-- ========================================
-- 第一步:删除重复的父菜单(目录)
-- ========================================
-- 删除重复的"心理测评管理"目录保留menu_id最小的
DELETE t1 FROM sys_menu t1
INNER JOIN sys_menu t2
WHERE t1.menu_name = '心理测评管理'
AND t1.parent_id = 0
AND t2.menu_name = '心理测评管理'
AND t2.parent_id = 0
AND t1.menu_id > t2.menu_id;
-- 删除重复的"心理网站管理"目录
DELETE t1 FROM sys_menu t1
INNER JOIN sys_menu t2
WHERE t1.menu_name = '心理网站管理'
AND t1.parent_id = 0
AND t2.menu_name = '心理网站管理'
AND t2.parent_id = 0
AND t1.menu_id > t2.menu_id;
-- ========================================
-- 第二步删除基于menu_name和parent_id的重复菜单
-- ========================================
-- 对于相同名称和父菜单的重复项保留最小的menu_id
DELETE t1 FROM sys_menu t1
INNER JOIN sys_menu t2
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的重复菜单
-- ========================================
-- 删除所有重复菜单保留menu_id最小的
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 '%规则%');
-- ========================================
-- 第四步:清理孤立的子菜单(父菜单已被删除)
-- ========================================
-- 删除那些父菜单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 '%规则%');
-- ========================================
-- 第五步:清理角色菜单关联表中的孤立记录
-- ========================================
-- 删除指向已删除菜单的角色菜单关联
DELETE FROM sys_role_menu
WHERE menu_id NOT IN (SELECT menu_id FROM (SELECT menu_id FROM sys_menu) AS temp2);
-- ========================================
-- 第六步:验证清理结果
-- ========================================
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 '%规则%'
GROUP BY menu_name, path, component, parent_id
HAVING COUNT(*) > 1;
-- 如果没有输出,说明没有重复菜单了
SELECT '如果上面的查询没有返回结果,说明所有重复菜单已清理完成!' AS message;

View File

@ -0,0 +1,58 @@
-- ========================================
-- 启用用户注册功能和添加量表权限管理菜单
-- ========================================
USE ry_news;
SET NAMES utf8mb4;
-- ========================================
-- 1. 启用用户注册功能
-- ========================================
-- 检查并更新注册功能配置
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, remark)
VALUES ('用户注册开关', 'sys.account.registerUser', 'true', 'Y', 'admin', NOW(), '是否开启用户注册功能true开启 false关闭')
ON DUPLICATE KEY UPDATE config_value = 'true', update_by = 'admin', update_time = NOW();
-- ========================================
-- 2. 量表权限管理菜单配置
-- ========================================
-- 量表权限管理主菜单
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '量表权限管理', menu_id, 11, 'permission', 'psychology/permission/index', 1, 0, 'C', '0', '0', 'psychology:permission:list', 'lock', 'admin', NOW(), '量表权限管理菜单'
FROM sys_menu WHERE menu_name = '心理测评管理' AND parent_id = 0 LIMIT 1;
-- 量表权限管理按钮权限
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限查询', menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:query', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限新增', menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:add', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限修改', menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:edit', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限删除', menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:remove', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
-- ========================================
-- 3. 为管理员角色分配量表权限管理菜单权限
-- ========================================
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, menu_id FROM sys_menu
WHERE (menu_name = '量表权限管理'
OR menu_name LIKE '%权限查询%'
OR menu_name LIKE '%权限新增%'
OR menu_name LIKE '%权限修改%'
OR menu_name LIKE '%权限删除%')
AND menu_name LIKE '%权限%';
-- ========================================
-- 验证配置
-- ========================================
SELECT '用户注册功能和量表权限管理菜单配置完成!' AS result;
SELECT config_value AS register_enabled FROM sys_config WHERE config_key = 'sys.account.registerUser';
SELECT COUNT(*) AS permission_menu_count FROM sys_menu WHERE menu_name LIKE '%量表权限%' OR menu_name LIKE '%权限%';

View File

@ -0,0 +1,38 @@
-- ========================================
-- 量表权限管理菜单配置
-- ========================================
USE ry_news;
SET NAMES utf8mb4;
-- ========================================
-- 量表权限管理
-- ========================================
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '量表权限管理', menu_id, 11, 'permission', 'psychology/permission/index', 1, 0, 'C', '0', '0', 'psychology:permission:list', 'lock', 'admin', NOW(), '量表权限管理菜单'
FROM sys_menu WHERE menu_name = '心理测评管理' AND parent_id = 0 LIMIT 1;
-- 量表权限管理按钮权限
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限查询', menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:query', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限新增', menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:add', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限修改', menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:edit', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
INSERT IGNORE INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
SELECT '权限删除', menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'psychology:permission:remove', '#', 'admin', NOW(), ''
FROM sys_menu WHERE menu_name = '量表权限管理' LIMIT 1;
-- ========================================
-- 为管理员角色分配权限
-- ========================================
INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '量表权限管理' OR menu_name LIKE '%权限查询%' OR menu_name LIKE '%权限新增%' OR menu_name LIKE '%权限修改%' OR menu_name LIKE '%权限删除%';
SELECT '量表权限管理菜单配置完成!' AS result;

View File

@ -0,0 +1,301 @@
# 14-用户注册和量表权限管理功能开发
## 功能概述
本功能模块实现了用户自主注册和量表权限管理功能,允许用户自行注册账号,管理员可以为用户分配量表访问权限,普通用户只能访问被分配权限的量表进行测评。
## 开发时间
2025-11-06
## 功能特性
### 1. 用户注册功能
- 用户可以通过注册页面自行注册账号
- 注册功能基于RuoYi框架现有的注册功能
- 注册成功后,用户可以使用注册的账号登录系统
### 2. 量表权限管理
- 管理员可以管理用户对量表的访问权限
- 支持按用户、角色、部门分配权限
- 支持权限的有效期设置(开始时间、结束时间)
- 支持权限状态管理(有效/无效)
### 3. 权限过滤
- 普通用户在选择量表时,只能看到被分配权限的量表
- 管理员可以看到所有量表
- 如果用户没有分配任何权限,会提示联系管理员
## 数据库设计
### 量表权限表psy_scale_permission
该表已在 `sql/psychological_system_complete.sql` 中定义:
```sql
CREATE TABLE `psy_scale_permission` (
`permission_id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`scale_id` bigint NOT NULL COMMENT '量表ID',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID关联sys_dept为空表示所有部门',
`role_id` bigint DEFAULT NULL COMMENT '角色ID关联sys_role为空表示所有角色',
`user_id` bigint DEFAULT NULL COMMENT '用户ID关联sys_user为空表示所有用户',
`class_name` varchar(100) DEFAULT NULL COMMENT '班级名称(特殊字段,用于罪犯管理)',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`status` char(1) DEFAULT '0' COMMENT '状态0有效 1无效',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`permission_id`),
KEY `idx_scale_id` (`scale_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='量表权限表';
```
## 后端开发
### 1. 实体类
**文件**: `ry-news-system/src/main/java/com/ddnai/system/domain/psychology/PsyScalePermission.java`
- 定义了量表权限的所有字段
- 包含关联查询字段scaleName, deptName, roleName, userName
### 2. Mapper接口和XML
**文件**:
- `ry-news-system/src/main/java/com/ddnai/system/mapper/psychology/PsyScalePermissionMapper.java`
- `ry-news-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml`
**核心方法**:
- `selectPermissionList`: 查询权限列表
- `selectScaleIdsByUserId`: 根据用户ID查询用户有权限访问的量表ID列表
- `checkUserScalePermission`: 检查用户是否有权限访问指定量表
- `batchAssignUserScalePermission`: 批量分配用户量表权限
### 3. Service接口和实现
**文件**:
- `ry-news-system/src/main/java/com/ddnai/system/service/psychology/IPsyScalePermissionService.java`
- `ry-news-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyScalePermissionServiceImpl.java`
**核心功能**:
- 权限查询和管理
- 用户权限检查(管理员自动拥有所有权限)
- 批量权限分配
### 4. Controller
**文件**: `ry-news-admin/src/main/java/com/ddnai/web/controller/psychology/PsyScalePermissionController.java`
**API接口**:
- `GET /psychology/permission/list` - 获取权限列表
- `GET /psychology/permission/{permissionId}` - 获取权限详情
- `GET /psychology/permission/user/{userId}/scales` - 获取用户有权限的量表ID列表
- `GET /psychology/permission/check/{userId}/{scaleId}` - 检查用户是否有权限
- `POST /psychology/permission` - 新增权限
- `PUT /psychology/permission` - 修改权限
- `DELETE /psychology/permission/{permissionIds}` - 删除权限
- `POST /psychology/permission/assign` - 批量分配用户量表权限
## 前端开发
### 1. API文件
**文件**: `ruoyi-ui/src/api/psychology/permission.js`
提供了所有权限相关的API调用方法。
### 2. 量表权限管理页面
**文件**: `ruoyi-ui/src/views/psychology/permission/index.vue`
**功能**:
- 权限列表查询(支持按量表、用户、状态筛选)
- 新增权限
- 修改权限
- 删除权限
- 显示权限详细信息(量表名称、用户名称、部门、角色、时间范围等)
### 3. 用户量表权限分配页面
**文件**: `ruoyi-ui/src/views/psychology/permission/user.vue`
**功能**:
- 使用Transfer组件进行量表权限分配
- 显示用户名称
- 支持搜索和筛选量表
- 批量保存权限
### 4. 用户管理页面集成
**文件**: `ruoyi-ui/src/views/system/user/index.vue`
**修改**:
- 在用户操作下拉菜单中添加"分配量表权限"选项
- 点击后跳转到用户量表权限分配页面
### 5. 量表选择页面权限过滤
**文件**: `ruoyi-ui/src/views/psychology/assessment/start.vue`
**修改**:
- 管理员userId === 1显示所有量表
- 普通用户只显示有权限的量表
- 如果用户没有权限,显示提示信息
### 6. 路由配置
**文件**: `ruoyi-ui/src/router/index.js`
**新增路由**:
- `/psychology/permission` - 量表权限管理
- `/psychology/permission/user/:userId` - 用户量表权限分配
## 菜单配置
`sql/psychological_system_complete.sql` 中添加了量表权限管理的菜单配置:
- **菜单名称**: 量表权限管理
- **路径**: `permission`
- **组件**: `psychology/permission/index`
- **权限标识**: `psychology:permission:list`
**按钮权限**:
- `psychology:permission:query` - 权限查询
- `psychology:permission:add` - 权限新增
- `psychology:permission:edit` - 权限修改
- `psychology:permission:remove` - 权限删除
## 使用方法
### 1. 用户注册
1. 访问系统注册页面(通常在登录页面有注册链接)
2. 填写用户名、密码、确认密码
3. 输入验证码(如果启用)
4. 点击注册按钮
5. 注册成功后,使用注册的账号登录系统
### 2. 管理员分配量表权限
#### 方法一:通过量表权限管理页面
1. 登录管理员账号
2. 进入"心理测评管理" -> "量表权限管理"
3. 点击"新增"按钮
4. 选择量表、用户(可选)、开始时间、结束时间、状态
5. 点击"确定"保存
#### 方法二:通过用户管理页面
1. 登录管理员账号
2. 进入"系统管理" -> "用户管理"
3. 找到要分配权限的用户
4. 点击操作列的"更多" -> "分配量表权限"
5. 在权限分配页面使用Transfer组件选择要分配的量表
6. 点击"保存"按钮
### 3. 用户选择量表进行测评
1. 用户登录系统
2. 进入"心理测评管理" -> "测评管理"
3. 点击"开始测评"按钮
4. 在量表选择页面,只能看到被分配权限的量表
5. 选择量表后,继续完成测评流程
## 权限规则
### 权限匹配规则
系统按以下优先级检查用户是否有权限访问量表:
1. **用户直接权限**: 如果存在 `user_id = 当前用户ID` 的权限记录
2. **角色权限**: 如果存在 `role_id = 用户角色ID` 的权限记录
3. **部门权限**: 如果存在 `dept_id = 用户部门ID` 的权限记录
4. **全局权限**: 如果存在 `user_id`, `role_id`, `dept_id` 都为空的权限记录(表示所有用户)
### 时间范围检查
- 如果权限设置了 `start_time`,当前时间必须 >= `start_time`
- 如果权限设置了 `end_time`,当前时间必须 <= `end_time`
- 如果时间字段为空,表示无时间限制
### 状态检查
- 只有 `status = '0'`(有效)的权限才会生效
### 管理员权限
- 管理员userId = 1自动拥有所有量表的访问权限不受权限表限制
## 注意事项
1. **注册功能**: 需要确保系统配置中启用了注册功能(`sys.account.registerUser = true`
2. **权限分配**: 建议在用户注册后,管理员及时为用户分配量表权限
3. **权限清理**: 删除用户时,会自动删除该用户的所有权限(通过外键约束)
4. **权限冲突**: 如果用户同时拥有多种类型的权限(用户、角色、部门),只要满足其中一种即可访问
5. **性能优化**: 权限查询使用了索引,但在大量用户和量表的情况下,建议定期清理无效权限
## 文件清单
### 后端文件
- `ry-news-system/src/main/java/com/ddnai/system/domain/psychology/PsyScalePermission.java`
- `ry-news-system/src/main/java/com/ddnai/system/mapper/psychology/PsyScalePermissionMapper.java`
- `ry-news-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml`
- `ry-news-system/src/main/java/com/ddnai/system/service/psychology/IPsyScalePermissionService.java`
- `ry-news-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyScalePermissionServiceImpl.java`
- `ry-news-admin/src/main/java/com/ddnai/web/controller/psychology/PsyScalePermissionController.java`
### 前端文件
- `ruoyi-ui/src/api/psychology/permission.js`
- `ruoyi-ui/src/views/psychology/permission/index.vue`
- `ruoyi-ui/src/views/psychology/permission/user.vue`
- `ruoyi-ui/src/views/system/user/index.vue` (修改)
- `ruoyi-ui/src/views/psychology/assessment/start.vue` (修改)
- `ruoyi-ui/src/router/index.js` (修改)
### 配置文件
- `sql/psychological_system_complete.sql` (菜单配置)
## 测试建议
1. **注册功能测试**
- 测试正常注册流程
- 测试用户名重复、密码格式验证
- 测试验证码功能
2. **权限分配测试**
- 测试通过权限管理页面分配权限
- 测试通过用户管理页面分配权限
- 测试批量分配权限
- 测试权限的时间范围限制
3. **权限过滤测试**
- 测试普通用户只能看到有权限的量表
- 测试管理员可以看到所有量表
- 测试无权限用户的提示信息
4. **权限检查测试**
- 测试用户直接权限
- 测试角色权限
- 测试部门权限
- 测试全局权限
- 测试权限过期情况
## 后续优化建议
1. **批量操作**: 支持批量为用户分配多个量表权限
2. **权限模板**: 支持创建权限模板,快速为多个用户分配相同权限
3. **权限统计**: 添加权限使用统计功能
4. **权限审批**: 支持权限申请和审批流程
5. **权限继承**: 支持部门权限继承到子部门
6. **权限导出**: 支持导出用户权限列表
## 总结
本功能模块完整实现了用户注册和量表权限管理功能管理员可以灵活地为用户分配量表访问权限普通用户只能访问被分配权限的量表有效控制了系统的访问范围。功能基于RuoYi框架开发遵循了框架的设计规范和代码风格易于维护和扩展。

View File

@ -0,0 +1,399 @@
# 新量表导入完整操作指南
## 📋 概述
本文档详细说明如何将一个新的心理量表完整导入到系统中,包括所有必要的配置步骤。
## 🎯 导入流程概览
```
1. 创建量表基本信息
2. 添加题目(题目管理)
3. 为每个题目添加选项(选项管理)
4. 添加因子(因子管理)
5. 配置因子计分规则(计分规则管理)
6. 配置结果解释(解释配置)
7. 配置预警规则(可选,预警规则配置)
8. 启用量表并测试
```
---
## 📝 详细操作步骤
### 步骤1创建量表基本信息
**路径**`心理测评管理` → `量表管理`
**操作步骤**
1. 点击 **"新增"** 按钮
2. 填写量表基本信息:
- **量表编码**:唯一标识,如 `SCL_90`、`EPQ_001`(必填)
- **量表名称**:如 `症状自评量表SCL-90`(必填)
- **量表类型**:选择类型(情绪、人格、行为等)
- **量表简介**:简短介绍
- **量表描述**:详细描述
- **题目数量**:预计题目数(如 `90`
- **预计完成时间**:分钟数(如 `30`
- **适用人群**:如 `一般人群`、`青少年`等
- **作者**:量表作者
- **来源**:量表来源
- **状态**:选择 `正常`(启用)或 `停用`
3. 点击 **"确定"** 保存
**注意事项**
- 量表编码必须唯一
- 状态选择 `正常` 后,量表才会在测评选择页面显示
- 题目数量可以后续修改
---
### 步骤2添加题目题目管理
**路径**`心理测评管理` → `量表管理` → 点击量表行的 **"题目管理"** 按钮
**操作步骤**
1. 在量表列表中找到刚创建的量表
2. 点击该行的 **"题目管理"** 按钮
3. 进入题目管理页面后,点击 **"新增"** 按钮
4. 填写题目信息:
- **题目序号**:如 `1`、`2`、`3`...(必填,用于排序)
- **题目内容**:题目的完整文字内容(必填)
- **题目类型**:选择 `单选题``多选题`
- **是否必答**:选择 `是``否`
- **排序**:数字越小越靠前(可选)
5. 点击 **"确定"** 保存
6. 重复步骤3-5添加所有题目
**批量添加技巧**
- 可以一次性添加多个题目,然后逐个配置选项
- 建议先添加所有题目,再统一配置选项
**注意事项**
- 题目序号建议连续从1开始
- 题目内容支持富文本(如果需要)
- 题目类型决定了后续选项的配置方式
---
### 步骤3为每个题目添加选项选项管理
**路径**:在题目管理页面,点击题目行的 **"选项管理"** 按钮
**操作步骤**
1. 在题目列表中,找到要配置选项的题目
2. 点击该行的 **"选项管理"** 按钮
3. 在弹出的对话框中,点击 **"新增"** 按钮
4. 填写选项信息:
- **选项编码**:如 `A`、`B`、`C`、`D` 或 `1`、`2`、`3`、`4`(必填)
- **选项内容**:选项的文字内容(必填)
- **选项分数**:该选项对应的分数(必填,如 `0`、`1`、`2`、`3`
- **排序**:数字越小越靠前(可选)
5. 点击 **"确定"** 保存
6. 重复步骤3-5为该题目添加所有选项
7. 点击对话框的 **"关闭"** 按钮
8. 重复步骤1-7为所有题目配置选项
**选项配置示例**
**单选题示例**(如焦虑自评量表):
- 选项A没有或很少时间分数1
- 选项B小部分时间分数2
- 选项C相当多时间分数3
- 选项D绝大部分或全部时间分数4
**多选题示例**(如症状自评量表):
- 选项A没有分数0
- 选项B很轻分数1
- 选项C中等分数2
- 选项D偏重分数3
- 选项E严重分数4
**注意事项**
- 选项编码在同一题目内必须唯一
- 选项分数用于后续计分,必须准确
- 单选题通常每个选项对应一个分数
- 多选题可能需要累加分数或取最高分
---
### 步骤4添加因子因子管理
**路径**`心理测评管理` → `量表管理` → 点击量表行的 **"因子管理"** 按钮
**操作步骤**
1. 在量表列表中找到量表
2. 点击该行的 **"因子管理"** 按钮
3. 进入因子管理页面后,点击 **"新增"** 按钮
4. 填写因子信息:
- **因子编码**:如 `F1`、`F2`、`F3` 或 `躯体化`、`强迫`等(必填)
- **因子名称**:如 `躯体化因子`、`强迫症状因子`(必填)
- **因子描述**:因子的详细说明(可选)
- **排序**:数字越小越靠前(可选)
5. 点击 **"确定"** 保存
6. 重复步骤3-5添加所有因子
**因子配置示例**如SCL-90量表
- 因子1躯体化
- 因子2强迫症状
- 因子3人际关系敏感
- 因子4抑郁
- 因子5焦虑
- 因子6敌对
- 因子7恐怖
- 因子8偏执
- 因子9精神病性
**注意事项**
- 因子编码在同一量表内必须唯一
- 因子用于后续的计分规则配置和结果解释
- 如果量表没有因子(只有总分),可以跳过此步骤
---
### 步骤5配置因子计分规则计分规则管理
**路径**:在因子管理页面,点击因子行的 **"计分规则"** 按钮
**操作步骤**
1. 在因子列表中,找到要配置计分规则的因子
2. 点击该行的 **"计分规则"** 按钮
3. 在弹出的对话框中,点击 **"新增规则"** 按钮
4. 在表格中配置规则:
- **题目**:从下拉框中选择题目(显示题目序号和内容)
- **选项**:从下拉框中选择选项(多选,可选,留空表示所有选项)
- **权重**该规则在计分中的权重0-10默认1.00
- **计算方式**:选择 `求和`、`平均`、`最大`、`最小`
5. 重复步骤3-4为该因子添加所有计分规则
6. 点击 **"保存"** 按钮保存所有规则
7. 重复步骤1-6为所有因子配置计分规则
**计分规则配置示例**
**示例1简单求和**如SCL-90的躯体化因子
- 规则1题目1 → 所有选项 → 权重1.00 → 计算方式:求和
- 规则2题目4 → 所有选项 → 权重1.00 → 计算方式:求和
- 规则3题目12 → 所有选项 → 权重1.00 → 计算方式:求和
- ...(包含所有属于该因子的题目)
**示例2加权求和**(如某些量表的因子):
- 规则1题目1 → 选项A,B → 权重2.00 → 计算方式:求和
- 规则2题目2 → 所有选项 → 权重1.00 → 计算方式:求和
**注意事项**
- 计分规则决定了因子分数的计算方式
- 如果某个题目属于多个因子,需要为每个因子都配置规则
- 计算方式:
- **求和**:所有选中选项的分数之和 × 权重
- **平均**:所有选中选项的分数平均值 × 权重
- **最大**:所有选中选项的分数最大值 × 权重
- **最小**:所有选中选项的分数最小值 × 权重
---
### 步骤6配置结果解释解释配置
**路径**`心理测评管理` → `解释配置`
**操作步骤**
1. 进入解释配置页面
2. 点击 **"新增"** 按钮
3. 填写解释信息:
- **量表**:从下拉框中选择量表(可选,留空表示通用解释)
- **因子**:从下拉框中选择因子(可选,留空表示总体解释;需先选择量表)
- **分数下限**:该解释适用的最低分数(必填)
- **分数上限**:该解释适用的最高分数(必填)
- **等级**:如 `低`、`中`、`高`(可选)
- **等级名称**:如 `轻度`、`中度`、`重度`(可选)
- **解释标题**:解释的标题(必填)
- **解释内容**:详细的解释说明(可选)
- **建议指导**:针对该分数段的建议或指导(可选)
- **排序**:数字越小越靠前(可选)
4. 点击 **"确定"** 保存
5. 重复步骤2-4为所有分数段配置解释
**结果解释配置示例**如SCL-90的躯体化因子
- 解释1量表=SCL-90因子=躯体化,分数范围=0-10等级=低,解释标题="正常范围"
- 解释2量表=SCL-90因子=躯体化,分数范围=11-20等级=中,解释标题="轻度症状"
- 解释3量表=SCL-90因子=躯体化,分数范围=21-30等级=高,解释标题="中度症状"
- 解释4量表=SCL-90因子=躯体化,分数范围=31-40等级=高,解释标题="重度症状"
**注意事项**
- 分数范围不能重叠(系统会根据分数自动匹配解释)
- 可以为量表总体配置解释(因子留空)
- 可以为特定因子配置解释(选择因子)
- 可以为通用情况配置解释(量表和因子都留空)
---
### 步骤7配置预警规则可选
**路径**`心理测评管理` → `预警规则配置`
**操作步骤**
1. 进入预警规则配置页面
2. 点击 **"新增"** 按钮
3. 填写预警规则信息:
- **量表**:从下拉框中选择量表(必填)
- **因子**:从下拉框中选择因子(可选,留空表示针对总分)
- **规则名称**:如 `重度抑郁预警`(必填)
- **预警等级**:选择 `低`、`中`、`高`、`紧急`(必填)
- **分数下限**:触发预警的最低分数(可选)
- **分数上限**:触发预警的最高分数(可选)
- **百分位下限**:触发预警的最低百分位(可选)
- **百分位上限**:触发预警的最高百分位(可选)
- **自动解除**:是否自动解除预警(可选)
- **解除条件**:自动解除的条件(可选)
- **状态**:选择 `正常`(启用)或 `停用`
4. 点击 **"确定"** 保存
5. 重复步骤2-4配置所有预警规则
**预警规则配置示例**
- 规则1量表=SCL-90因子=抑郁,预警等级=高,分数范围=30-40状态=正常
- 规则2量表=SCL-90因子=焦虑,预警等级=紧急,分数范围=35-40状态=正常
**注意事项**
- 预警规则用于在测评结果异常时自动向管理员发出警告
- 可以针对特定因子配置,也可以针对总分配置
- 预警等级决定了警告的严重程度
- 只有状态为 `正常` 的规则才会生效
---
### 步骤8启用量表并测试
**操作步骤**
1. 返回 `量表管理` 页面
2. 确认量表状态为 `正常`(如果为 `停用`,点击修改改为 `正常`
3. 确认所有配置已完成:
- ✅ 量表基本信息已填写
- ✅ 题目已添加至少1题
- ✅ 每个题目都有选项至少1个选项
- ✅ 因子已添加(如果有因子)
- ✅ 因子计分规则已配置(如果有因子)
- ✅ 结果解释已配置至少1条
4. 测试量表:
- 进入 `测评管理``开始测评`
- 选择刚创建的量表
- 填写被测评人信息
- 开始答题,完成所有题目
- 提交测评
- 查看生成的报告,验证:
- 分数计算是否正确
- 因子分数是否正确(如果有因子)
- 结果解释是否正确显示
- 预警是否正常触发(如果配置了预警规则)
---
## 📊 配置检查清单
在完成量表导入后,使用以下清单检查配置是否完整:
### 基本信息
- [ ] 量表编码已填写且唯一
- [ ] 量表名称已填写
- [ ] 量表状态为 `正常`
- [ ] 题目数量与实际题目数一致
### 题目配置
- [ ] 至少添加了1个题目
- [ ] 所有题目都有题目序号
- [ ] 所有题目都有题目内容
- [ ] 题目类型已选择(单选/多选)
### 选项配置
- [ ] 每个题目至少添加了1个选项
- [ ] 所有选项都有选项编码
- [ ] 所有选项都有选项内容
- [ ] 所有选项都有选项分数
### 因子配置(如果有因子)
- [ ] 至少添加了1个因子
- [ ] 所有因子都有因子编码
- [ ] 所有因子都有因子名称
### 计分规则配置(如果有因子)
- [ ] 每个因子至少配置了1条计分规则
- [ ] 计分规则中的题目都已选择
- [ ] 计分规则中的权重已设置
- [ ] 计分规则中的计算方式已选择
### 结果解释配置
- [ ] 至少配置了1条结果解释
- [ ] 结果解释的分数范围已设置
- [ ] 结果解释的解释标题已填写
- [ ] 结果解释的解释内容已填写(建议)
### 预警规则配置(可选)
- [ ] 预警规则已配置(如果需要)
- [ ] 预警规则的量表已选择
- [ ] 预警规则的预警等级已选择
- [ ] 预警规则的状态为 `正常`
---
## 🔍 常见问题
### Q1量表创建后在测评选择页面看不到
**A**:检查以下几点:
- 量表状态是否为 `正常`
- 量表是否有至少1个题目
- 刷新浏览器页面
### Q2题目添加后选项管理按钮点击没反应
**A**:检查以下几点:
- 题目是否已保存
- 浏览器控制台是否有错误
- 尝试刷新页面
### Q3因子计分规则配置后分数计算不正确
**A**:检查以下几点:
- 选项分数是否正确
- 计分规则中的题目是否选择正确
- 计分规则中的计算方式是否合适
- 权重设置是否正确
### Q4结果解释不显示
**A**:检查以下几点:
- 是否配置了结果解释
- 结果解释的分数范围是否覆盖了实际分数
- 结果解释的量表和因子是否匹配
### Q5预警规则不触发
**A**:检查以下几点:
- 预警规则的状态是否为 `正常`
- 预警规则的分数范围是否设置正确
- 测评结果是否满足预警条件
---
## 📚 相关文档
- [功能使用指南](./使用指南-功能测试说明.md)
- [用户注册和量表权限管理功能开发](./14-用户注册和量表权限管理功能开发.md)
---
## ✅ 完成标志
当以下所有条件满足时,量表导入完成:
1. ✅ 量表在测评选择页面可以正常选择
2. ✅ 可以正常开始测评并答题
3. ✅ 提交测评后可以正常生成报告
4. ✅ 报告中的分数计算正确
5. ✅ 报告中的结果解释正确显示
6. ✅ 预警规则正常触发(如果配置了)
---
**最后更新**2025-11-06