feat: 角色等级体系与权限管控实施

- 数据库:auth_roles 表新增 level 字段(1=超管, 10=管理员, 100=普通用户)
- 后端:新增 GetAllRoles/GetUserMaxLevel/SetUserRoles/FindByCodeList DAO 方法
- 后端:SetRolesRequest 替换 AssignRoleRequest,AdminUserInfo.Roles 改为 []RoleInfo
- 后端:所有管理操作(禁用/启用/角色分配/创建用户)强制层级权限校验
- 后端:PUT /users/:id/roles(批量设置角色)+ GET /roles(角色列表)
- 前端:角色管理改为 Checkbox Group 多选,高等级角色禁用
- 前端:禁用/启用按钮受角色层级约束,列表页操作按钮权限管控
- 前端:创建用户对话框角色选项根据操作者等级动态过滤
- 文档:API 文档、设计方案、集成规范、进度文档同步更新

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-03-02 15:40:02 +08:00
parent dd84e50429
commit fa6f1e5c58
18 changed files with 771 additions and 279 deletions

View File

@@ -27,14 +27,15 @@ EchoChat 是一个实时音视频通讯平台,包含三个子项目:
## 核心开发规则
1. **前端设计**:必须使用 `ui-ux-pro-max` 技能包`~/.agent/skills/ui-ux-pro-max/scripts/search.py`禁止手动设计
1. **前端设计**:必须使用 openskills 安装的 `ui-ux-pro-max` 技能包,脚本绝对路径为 `/Users/bojinyuan/.agent/skills/ui-ux-pro-max/scripts/search.py`。**严禁使用** `.cursor/skills/ui-ux-pro-max.bak/` 目录下的任何文件,该目录已废弃。禁止手动设计系统
2. **工作流**:使用 superpowers 流程控制开发节奏
3. **两端差异**`frontend/` 和 `admin/` 是完全独立的项目,技术栈不需要统一
4. **模块系统**:前端统一使用 ESM`export`/`import`),禁止 CommonJS
5. **Go 常量命名**camelCase`UserStatusActive`),非大写下划线
6. **API 响应**:统一 `{ "code": 0, "message": "success", "data": ... }`
7. **JWT 策略**:有状态 JWTToken 存 Redis按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响
8. **代码注释**所有公开函数、组件、Store 必须有详细注释
8. **角色等级体系**`auth_roles.level` 字段值越小权限越高1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行层级权限校验
9. **代码注释**所有公开函数、组件、Store 必须有详细注释
9. **文档同步**:代码变更后必须同步更新 docs/ 下相关文档
10. **验证方式**:使用 Playwright MCP 进行页面自动化验证
11. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性

View File

@@ -1,15 +1,16 @@
/**
* 用户管理 API 模块
*
* 对应后端路由:/api/v1/admin/users
* 对应后端路由:/api/v1/admin/users, /api/v1/admin/roles
* 所有接口需要 JWT + admin 角色权限
*
* 接口列表:
* - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选)
* - GET /api/v1/admin/users/:id 用户详情
* - PUT /api/v1/admin/users/:id/status 更新用户状态
* - PUT /api/v1/admin/users/:id/role 分配角色
* - PUT /api/v1/admin/users/:id/roles 批量设置角色
* - POST /api/v1/admin/users 管理员创建用户
* - GET /api/v1/admin/roles 获取所有角色列表
*/
import request from '@/utils/request'
@@ -57,16 +58,27 @@ export function updateUserStatus(id, status) {
}
/**
* 分配角色
* 批量设置用户角色(先清后设)
* @param {number} id - 用户 ID
* @param {string} roleCode - 角色代码user/admin/super_admin
* @param {string[]} roleCodes - 角色代码列表
* @returns {Promise}
*/
export function assignUserRole(id, roleCode) {
export function setUserRoles(id, roleCodes) {
return request({
url: `/api/v1/admin/users/${id}/role`,
url: `/api/v1/admin/users/${id}/roles`,
method: 'put',
data: { role_code: roleCode }
data: { role_codes: roleCodes }
})
}
/**
* 获取所有角色列表(含 level 等级信息)
* @returns {Promise<{data: Array<{code: string, name: string, level: number}>}>}
*/
export function getAllRoles() {
return request({
url: '/api/v1/admin/roles',
method: 'get'
})
}

View File

@@ -5,14 +5,15 @@
功能
- 基本信息展示ID用户名邮箱昵称性别手机状态注册/更新时间
- 角色管理查看当前角色 + 分配新角色
- 状态切换启用/禁用
- 角色管理Checkbox Group 多选权限等级管控
- 状态切换启用/禁用受角色层级约束
- 会议记录 Tab占位后续完善
对应后端 API
- GET /api/v1/admin/users/:id
- PUT /api/v1/admin/users/:id/status
- PUT /api/v1/admin/users/:id/role
- PUT /api/v1/admin/users/:id/roles
- GET /api/v1/admin/roles
-->
<template>
<div class="user-detail-page" v-loading="loading">
@@ -43,7 +44,7 @@
{{ userInfo.status_text }}
</el-tag>
</div>
<div class="user-actions">
<div class="user-actions" v-if="canManageTarget">
<el-button
v-if="userInfo.status === 1"
type="danger"
@@ -59,6 +60,9 @@
启用账号
</el-button>
</div>
<div class="user-actions" v-else>
<el-tag type="info" size="small">权限不足无法操作该用户</el-tag>
</div>
</div>
<!-- 详细信息表格 -->
@@ -80,45 +84,39 @@
<el-tab-pane label="角色管理" name="roles">
<el-card shadow="never" class="role-card">
<div class="role-section">
<h4 class="section-title">当前角色</h4>
<div class="current-roles">
<el-tag
v-for="role in userInfo.roles"
:key="role"
:type="getRoleTagType(role)"
size="large"
class="role-tag"
<h4 class="section-title">角色分配</h4>
<p class="role-hint" v-if="!canManageTarget" style="margin-bottom: 12px">
该用户权限等级高于或等于您无法修改其角色
</p>
<el-checkbox-group v-model="selectedRoleCodes" class="role-checkbox-group">
<el-checkbox
v-for="role in allRoles"
:key="role.code"
:value="role.code"
:disabled="isRoleDisabled(role)"
class="role-checkbox-item"
>
{{ getRoleName(role) }}
</el-tag>
<span v-if="!userInfo.roles || userInfo.roles.length === 0" class="no-role">
暂未分配角色
</span>
<div class="role-checkbox-label">
<el-tag :type="getRoleTagType(role.code)" size="small">{{ role.name }}</el-tag>
<span class="role-level-badge">Lv.{{ role.level }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
<p class="role-hint" style="margin-top: 12px">
灰色不可选的角色等级高于或等于您的权限无法分配勾选后点击"保存"生效
</p>
</div>
<el-divider />
<div class="role-section">
<h4 class="section-title">分配角色</h4>
<div class="assign-role-form">
<el-select v-model="selectedRole" placeholder="选择角色" style="width: 200px">
<el-option label="普通用户" value="user" />
<el-option label="管理员" value="admin" />
<el-option label="超级管理员" value="super_admin" />
</el-select>
<div class="role-actions" v-if="canManageTarget">
<el-button
type="primary"
:loading="roleLoading"
:disabled="!selectedRole"
@click="handleAssignRole"
:disabled="!hasRoleChanges"
@click="handleSaveRoles"
>
分配角色
保存角色
</el-button>
</div>
<p class="role-hint">
分配角色将为该用户添加所选角色权限已有角色不会受影响
</p>
<el-button @click="resetRoleSelection">重置</el-button>
</div>
</el-card>
</el-tab-pane>
@@ -143,103 +141,129 @@
*
* 数据流:
* 1. 从路由参数获取 userId
* 2. 调用 API 获取用户详情
* 3. 支持状态切换和角色分配操作
* 2. 并行调用 getUserDetail + getAllRoles 获取用户详情和角色列表
* 3. 通过比较操作者 level 和目标用户 level 控制权限
* 4. 角色分配使用 Checkbox Group 多选,高等级角色禁用
*/
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserDetail, updateUserStatus, assignUserRole } from '@/api/user'
import { getUserDetail, updateUserStatus, setUserRoles, getAllRoles } from '@/api/user'
import { useUserStore } from '@/store/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
/** 加载状态 */
const loading = ref(false)
/** 用户详情数据 */
const userInfo = ref(null)
/** 当前 Tab */
const activeTab = ref('info')
/** 角色分配选中值 */
const selectedRole = ref('')
/** 角色分配加载状态 */
const roleLoading = ref(false)
/** 路由参数中的用户 ID */
/** 系统所有角色列表(从 API 获取,含 level */
const allRoles = ref([])
/** Checkbox Group 绑定值:选中的角色 code 列表 */
const selectedRoleCodes = ref([])
/** 记录用户原始角色 codes用于比较是否有变更 */
const originalRoleCodes = ref([])
const userId = computed(() => Number(route.params.id))
/** 头像占位字母 */
const avatarLetter = computed(() => {
if (!userInfo.value) return '?'
const name = userInfo.value.nickname || userInfo.value.username || '?'
return name.charAt(0).toUpperCase()
})
/**
* 获取状态 Tag 类型
* @param {number} status
*/
/** 当前操作者的最高权限等级(最小 level 值) */
const adminMaxLevel = computed(() => {
const adminRoleCodes = userStore.roles || []
if (!allRoles.value.length || !adminRoleCodes.length) return 999
let minLevel = 999
for (const r of allRoles.value) {
if (adminRoleCodes.includes(r.code) && r.level < minLevel) {
minLevel = r.level
}
}
return minLevel
})
/** 目标用户的最高权限等级(后端无角色时返回 MaxInt32前端以 999 作为等效兜底) */
const targetMaxLevel = computed(() => {
if (!userInfo.value || userInfo.value.max_level == null) return 999
return userInfo.value.max_level
})
/** 操作者是否有权管理目标用户level 严格小于) */
const canManageTarget = computed(() => {
return adminMaxLevel.value < targetMaxLevel.value
})
/** Checkbox 是否有变更 */
const hasRoleChanges = computed(() => {
if (selectedRoleCodes.value.length !== originalRoleCodes.value.length) return true
const sorted1 = [...selectedRoleCodes.value].sort()
const sorted2 = [...originalRoleCodes.value].sort()
return sorted1.some((v, i) => v !== sorted2[i])
})
const getStatusType = (status) => {
const map = { 1: 'success', 2: 'danger', 3: 'info' }
return map[status] || 'info'
}
/**
* 获取性别文本
* @param {number} gender
*/
const getGenderText = (gender) => {
const map = { 0: '未知', 1: '男', 2: '女' }
return map[gender] || '未知'
}
/**
* 获取角色 Tag 类型
* @param {string} role
*/
const getRoleTagType = (role) => {
const map = { super_admin: 'danger', admin: 'warning', user: '' }
return map[role] || 'info'
}
/**
* 获取角色中文名称
* @param {string} role
* 判断角色 checkbox 是否禁用
* 规则:角色 level <= 操作者 level 时禁用(不能分配高于或等于自身等级的角色)
* 如果无法管理目标用户,全部禁用
*/
const getRoleName = (role) => {
const map = { super_admin: '超级管理员', admin: '管理员', user: '普通用户' }
return map[role] || role
const isRoleDisabled = (role) => {
if (!canManageTarget.value) return true
return role.level <= adminMaxLevel.value
}
/**
* 获取用户详情
*/
const fetchUserDetail = async () => {
/** 重置角色选择到原始状态 */
const resetRoleSelection = () => {
selectedRoleCodes.value = [...originalRoleCodes.value]
}
const fetchData = async () => {
loading.value = true
try {
const res = await getUserDetail(userId.value)
userInfo.value = res.data
const [userRes, rolesRes] = await Promise.all([
getUserDetail(userId.value),
getAllRoles()
])
userInfo.value = userRes.data
allRoles.value = rolesRes.data || []
const codes = (userRes.data.roles || []).map(r => r.code)
selectedRoleCodes.value = [...codes]
originalRoleCodes.value = [...codes]
} catch (err) {
console.error('获取用户详情失败:', err)
console.error('获取数据失败:', err)
userInfo.value = null
} finally {
loading.value = false
}
}
/** 返回列表 */
const goBack = () => {
router.push('/user/list')
}
/**
* 切换用户状态
* @param {number} newStatus
*/
const handleToggleStatus = async (newStatus) => {
const action = newStatus === 2 ? '禁用' : '启用'
try {
@@ -248,38 +272,33 @@ const handleToggleStatus = async (newStatus) => {
`${action}确认`,
{ type: 'warning' }
)
await updateUserStatus(userId.value, newStatus)
ElMessage.success(`${action}成功`)
fetchUserDetail()
fetchData()
} catch (err) {
if (err !== 'cancel') {
if (err !== 'cancel' && err !== 'close') {
console.error(`${action}失败:`, err)
}
}
}
/**
* 分配角色
*/
const handleAssignRole = async () => {
if (!selectedRole.value) return
const handleSaveRoles = async () => {
if (!hasRoleChanges.value) return
roleLoading.value = true
try {
await assignUserRole(userId.value, selectedRole.value)
ElMessage.success('角色分配成功')
selectedRole.value = ''
fetchUserDetail()
await setUserRoles(userId.value, selectedRoleCodes.value)
ElMessage.success('角色设置成功')
fetchData()
} catch (err) {
console.error('角色分配失败:', err)
console.error('角色设置失败:', err)
} finally {
roleLoading.value = false
}
}
onMounted(() => {
fetchUserDetail()
fetchData()
})
</script>
@@ -369,7 +388,7 @@ onMounted(() => {
}
.role-section {
margin-bottom: 8px;
margin-bottom: 16px;
}
.section-title {
@@ -379,27 +398,37 @@ onMounted(() => {
margin: 0 0 12px 0;
}
.current-roles {
.role-checkbox-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.role-tag {
font-size: 14px;
}
.no-role {
color: #94A3B8;
font-size: 14px;
}
.assign-role-form {
display: flex;
align-items: center;
flex-direction: column;
gap: 12px;
}
.role-checkbox-item {
height: auto;
}
.role-checkbox-label {
display: inline-flex;
align-items: center;
gap: 8px;
}
.role-level-badge {
font-size: 11px;
color: #94A3B8;
background: #F1F5F9;
padding: 1px 6px;
border-radius: 4px;
}
.role-actions {
display: flex;
gap: 12px;
padding-top: 16px;
border-top: 1px solid #E2E8F0;
}
.role-hint {
font-size: 12px;
color: #94A3B8;

View File

@@ -89,12 +89,12 @@
<template #default="{ row }">
<el-tag
v-for="role in row.roles"
:key="role"
:type="getRoleTagType(role)"
:key="role.code"
:type="getRoleTagType(role.code)"
size="small"
class="role-tag"
>
{{ getRoleName(role) }}
{{ role.name }}
</el-tag>
<span v-if="!row.roles || row.roles.length === 0" class="no-role">未分配</span>
</template>
@@ -121,6 +121,7 @@
<el-button type="primary" link size="small" @click.stop="goDetail(row.id)">
详情
</el-button>
<template v-if="canManageRow(row)">
<el-button
v-if="row.status === 1"
type="danger"
@@ -140,6 +141,7 @@
启用
</el-button>
</template>
</template>
</el-table-column>
</el-table>
@@ -185,8 +187,12 @@
</el-form-item>
<el-form-item label="角色" prop="role_code">
<el-select v-model="createForm.role_code" placeholder="选择角色" style="width: 100%">
<el-option label="普通用户" value="user" />
<el-option label="管理员" value="admin" />
<el-option
v-for="role in assignableRoles"
:key="role.code"
:label="role.name"
:value="role.code"
/>
</el-select>
</el-form-item>
</el-form>
@@ -207,12 +213,14 @@
* 2. 搜索/筛选/分页变化时重新加载
* 3. 操作(禁用/启用/创建)成功后刷新列表
*/
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserList, updateUserStatus, createUser } from '@/api/user'
import { getUserList, updateUserStatus, createUser, getAllRoles } from '@/api/user'
import { useUserStore } from '@/store/user'
const router = useRouter()
const userStore = useUserStore()
/** 加载状态 */
const loading = ref(false)
@@ -233,6 +241,36 @@ const pagination = reactive({
total: 0
})
/** 系统所有角色列表(从 API 获取,含 level */
const allRoles = ref([])
/** 当前操作者的最高权限等级(最小 level 值) */
const adminMaxLevel = computed(() => {
const adminRoleCodes = userStore.roles || []
if (!allRoles.value.length || !adminRoleCodes.length) return 999
let minLevel = 999
for (const r of allRoles.value) {
if (adminRoleCodes.includes(r.code) && r.level < minLevel) {
minLevel = r.level
}
}
return minLevel
})
/** 创建用户对话框中可选的角色列表(过滤掉高于操作者等级的角色) */
const assignableRoles = computed(() => {
return allRoles.value.filter(r => r.level > adminMaxLevel.value)
})
/**
* 判断操作者是否有权管理目标用户level 严格小于)
* @param {Object} row - 用户行数据(含 max_level
*/
const canManageRow = (row) => {
const targetLevel = (row.max_level == null) ? 999 : row.max_level
return adminMaxLevel.value < targetLevel
}
/** 创建用户对话框显示状态 */
const showCreateDialog = ref(false)
@@ -276,15 +314,6 @@ const getRoleTagType = (role) => {
return map[role] || 'info'
}
/**
* 获取角色中文名称
* @param {string} role - 角色代码
*/
const getRoleName = (role) => {
const map = { super_admin: '超级管理员', admin: '管理员', user: '普通用户' }
return map[role] || role
}
/**
* 获取状态 Tag 类型
* @param {number} status - 状态码
@@ -364,7 +393,7 @@ const handleToggleStatus = async (row, newStatus) => {
ElMessage.success(`${action}成功`)
fetchUserList()
} catch (err) {
if (err !== 'cancel') {
if (err !== 'cancel' && err !== 'close') {
console.error(`${action}用户失败:`, err)
}
}
@@ -405,8 +434,19 @@ const resetCreateForm = () => {
createForm.role_code = 'user'
}
/** 获取角色列表(用于权限判断和创建用户的角色选项) */
const fetchRoles = async () => {
try {
const res = await getAllRoles()
allRoles.value = res.data || []
} catch (err) {
console.error('获取角色列表失败:', err)
}
}
onMounted(() => {
fetchUserList()
fetchRoles()
})
</script>

View File

@@ -119,6 +119,8 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) {
utils.ResponseBadRequest(c, "不能禁用自己的账号")
case service.ErrInvalidStatus:
utils.ResponseBadRequest(c, "无效的用户状态")
case service.ErrInsufficientPermission:
utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户")
default:
logs.Error(ctx, funcName, "更新用户状态失败", zap.Error(err))
utils.ResponseError(c, "更新用户状态失败")
@@ -129,10 +131,10 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) {
utils.ResponseOK(c, nil)
}
// AssignRole 分配角色
// PUT /api/v1/admin/users/:id/role
func (ctl *UserManageController) AssignRole(c *gin.Context) {
funcName := "controller.user_manage_controller.AssignRole"
// SetRoles 批量设置用户角色
// PUT /api/v1/admin/users/:id/roles
func (ctl *UserManageController) SetRoles(c *gin.Context) {
funcName := "controller.user_manage_controller.SetRoles"
ctx := c.Request.Context()
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
@@ -141,27 +143,38 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) {
return
}
var req dto.AssignRoleRequest
var req dto.SetRolesRequest
if err := c.ShouldBindJSON(&req); err != nil {
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
utils.ResponseBadRequest(c, "参数错误: "+err.Error())
return
}
logs.Info(ctx, funcName, "分配角色",
zap.Int64("user_id", userID),
zap.String("role_code", req.RoleCode),
adminUserID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
logs.Info(ctx, funcName, "设置用户角色",
zap.Int64("target_user_id", userID),
zap.Strings("role_codes", req.RoleCodes),
zap.Int64("admin_user_id", adminUserID),
)
if err := ctl.userManageService.AssignUserRole(ctx, userID, req.RoleCode); err != nil {
if err := ctl.userManageService.SetUserRoles(ctx, userID, req.RoleCodes, adminUserID); err != nil {
switch err {
case service.ErrUserNotFound:
utils.ResponseNotFound(c, "用户不存在")
case service.ErrInvalidRole:
utils.ResponseBadRequest(c, "无效的角色代码")
utils.ResponseBadRequest(c, "包含无效的角色代码")
case service.ErrInsufficientPermission:
utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户")
case service.ErrCannotAssignHigherRole:
utils.ResponseForbidden(c, "不能分配高于自身等级的角色")
default:
logs.Error(ctx, funcName, "分配角色失败", zap.Error(err))
utils.ResponseError(c, "分配角色失败")
logs.Error(ctx, funcName, "设置用户角色失败", zap.Error(err))
utils.ResponseError(c, "设置用户角色失败")
}
return
}
@@ -169,6 +182,24 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) {
utils.ResponseOK(c, nil)
}
// GetAllRoles 获取所有角色列表
// GET /api/v1/admin/roles
func (ctl *UserManageController) GetAllRoles(c *gin.Context) {
funcName := "controller.user_manage_controller.GetAllRoles"
ctx := c.Request.Context()
logs.Info(ctx, funcName, "获取所有角色列表")
roles, err := ctl.userManageService.GetAllRoles(ctx)
if err != nil {
logs.Error(ctx, funcName, "获取角色列表失败", zap.Error(err))
utils.ResponseError(c, "获取角色列表失败")
return
}
utils.ResponseOK(c, roles)
}
// CreateUser 管理员手动创建用户
// POST /api/v1/admin/users
func (ctl *UserManageController) CreateUser(c *gin.Context) {
@@ -182,19 +213,31 @@ func (ctl *UserManageController) CreateUser(c *gin.Context) {
return
}
adminUserID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
logs.Info(ctx, funcName, "管理员创建用户",
zap.String("username", req.Username),
zap.String("email", logs.MaskEmail(req.Email)),
zap.Int64("admin_user_id", adminUserID),
)
userInfo, err := ctl.userManageService.CreateUser(ctx, &req)
userInfo, err := ctl.userManageService.CreateUser(ctx, &req, adminUserID)
if err != nil {
if err == service.ErrUserExists {
switch err {
case service.ErrUserExists:
utils.ResponseBadRequest(c, "用户名或邮箱已被注册")
return
}
case service.ErrInvalidRole:
utils.ResponseBadRequest(c, "无效的角色代码")
case service.ErrCannotAssignHigherRole:
utils.ResponseForbidden(c, "不能分配高于自身等级的角色")
default:
logs.Error(ctx, funcName, "创建用户失败", zap.Error(err))
utils.ResponseError(c, "创建用户失败")
}
return
}

View File

@@ -23,7 +23,10 @@ func RegisterRoutes(
adminGroup.GET("/users", ctrl.GetUserList)
adminGroup.GET("/users/:id", ctrl.GetUserDetail)
adminGroup.PUT("/users/:id/status", ctrl.UpdateUserStatus)
adminGroup.PUT("/users/:id/role", ctrl.AssignRole)
adminGroup.PUT("/users/:id/roles", ctrl.SetRoles)
adminGroup.POST("/users", ctrl.CreateUser)
// 角色管理
adminGroup.GET("/roles", ctrl.GetAllRoles)
}
}

View File

@@ -4,6 +4,7 @@ package service
import (
"context"
"errors"
"math"
authDAO "github.com/echochat/backend/app/auth/dao"
"github.com/echochat/backend/app/auth/model"
@@ -22,6 +23,8 @@ var (
ErrInvalidStatus = errors.New("无效的用户状态")
ErrInvalidRole = errors.New("无效的角色代码")
ErrCannotDisableSelf = errors.New("不能禁用自己的账号")
ErrInsufficientPermission = errors.New("权限不足,无法操作更高等级的用户")
ErrCannotAssignHigherRole = errors.New("不能分配高于自身等级的角色")
)
// UserManageService 管理端用户管理服务
@@ -56,11 +59,11 @@ func (s *UserManageService) GetUserList(ctx context.Context, req *dto.UserListRe
list := make([]dto.AdminUserInfo, 0, len(users))
for _, user := range users {
roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if roleErr != nil {
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
zap.Int64("user_id", user.ID), zap.Error(roleErr))
roles = []string{}
roles = []model.Role{}
}
list = append(list, *s.buildAdminUserInfo(&user, roles))
}
@@ -84,17 +87,17 @@ func (s *UserManageService) GetUserDetail(ctx context.Context, userID int64) (*d
return nil, err
}
roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if roleErr != nil {
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
zap.Int64("user_id", user.ID), zap.Error(roleErr))
roles = []string{}
roles = []model.Role{}
}
return s.buildAdminUserInfo(user, roles), nil
}
// UpdateUserStatus 启用/禁用用户
// adminUserID 用于防止管理员禁用自己
// adminUserID 用于防止管理员禁用自己;同时校验操作者权限等级必须高于目标用户
func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error {
funcName := "service.user_manage_service.UpdateUserStatus"
logs.Info(ctx, funcName, "更新用户状态",
@@ -111,7 +114,6 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
return ErrInvalidStatus
}
// 确认用户存在
_, err := s.userDAO.FindByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -120,6 +122,10 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
return err
}
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
return err
}
if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil {
return err
}
@@ -131,15 +137,16 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
return nil
}
// AssignUserRole 分配角色给用户
func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, roleCode string) error {
funcName := "service.user_manage_service.AssignUserRole"
logs.Info(ctx, funcName, "分配角色",
zap.Int64("user_id", userID),
zap.String("role_code", roleCode),
// SetUserRoles 批量设置用户角色(事务内先清后设)
// adminUserID 用于校验操作者等级,防止越权分配高等级角色
func (s *UserManageService) SetUserRoles(ctx context.Context, userID int64, roleCodes []string, adminUserID int64) error {
funcName := "service.user_manage_service.SetUserRoles"
logs.Info(ctx, funcName, "设置用户角色",
zap.Int64("target_user_id", userID),
zap.Strings("role_codes", roleCodes),
zap.Int64("admin_user_id", adminUserID),
)
// 验证用户存在
_, err := s.userDAO.FindByID(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -148,33 +155,106 @@ func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, ro
return err
}
// 查找角色
role, err := s.roleDAO.FindByCode(ctx, roleCode)
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
return err
}
roles, err := s.roleDAO.FindByCodeList(ctx, roleCodes)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if len(roles) != len(roleCodes) {
return ErrInvalidRole
}
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
return err
}
for _, r := range roles {
if r.Level <= adminLevel {
logs.Warn(ctx, funcName, "尝试分配高于自身等级的角色",
zap.String("role_code", r.Code),
zap.Int("role_level", r.Level),
zap.Int("admin_level", adminLevel),
)
return ErrCannotAssignHigherRole
}
}
roleIDs := make([]int, 0, len(roles))
for _, r := range roles {
roleIDs = append(roleIDs, r.ID)
}
if err := s.roleDAO.SetUserRoles(ctx, userID, roleIDs); err != nil {
return err
}
// 分配角色RoleDAO.AssignRole 内部会处理重复分配)
if err := s.roleDAO.AssignRole(ctx, userID, role.ID); err != nil {
return err
}
logs.Info(ctx, funcName, "角色分配成功",
logs.Info(ctx, funcName, "用户角色设置成功",
zap.Int64("user_id", userID),
zap.String("role_code", roleCode),
zap.Strings("role_codes", roleCodes),
)
return nil
}
// GetAllRoles 获取所有角色列表
func (s *UserManageService) GetAllRoles(ctx context.Context) ([]dto.RoleInfo, error) {
funcName := "service.user_manage_service.GetAllRoles"
logs.Info(ctx, funcName, "获取所有角色列表")
roles, err := s.roleDAO.GetAllRoles(ctx)
if err != nil {
return nil, err
}
result := make([]dto.RoleInfo, 0, len(roles))
for _, r := range roles {
result = append(result, dto.RoleInfo{
Code: r.Code,
Name: r.Name,
Level: r.Level,
})
}
return result, nil
}
// checkPermissionLevel 校验操作者的权限等级是否高于目标用户
// 操作者 level 必须 < 目标用户 level数值更小 = 权限更高)
func (s *UserManageService) checkPermissionLevel(ctx context.Context, adminUserID, targetUserID int64) error {
funcName := "service.user_manage_service.checkPermissionLevel"
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
logs.Error(ctx, funcName, "获取操作者权限等级失败", zap.Error(err))
return err
}
targetLevel, err := s.roleDAO.GetUserMaxLevel(ctx, targetUserID)
if err != nil {
logs.Error(ctx, funcName, "获取目标用户权限等级失败", zap.Error(err))
return err
}
if adminLevel >= targetLevel {
logs.Warn(ctx, funcName, "权限不足",
zap.Int64("admin_user_id", adminUserID),
zap.Int("admin_level", adminLevel),
zap.Int64("target_user_id", targetUserID),
zap.Int("target_level", targetLevel),
)
return ErrInsufficientPermission
}
return nil
}
// CreateUser 管理员手动创建用户
func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest) (*dto.AdminUserInfo, error) {
// adminUserID 用于校验操作者权限等级,防止越权分配高等级角色
func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreateUserRequest, adminUserID int64) (*dto.AdminUserInfo, error) {
funcName := "service.user_manage_service.CreateUser"
logs.Info(ctx, funcName, "管理员创建用户",
zap.String("username", req.Username),
zap.String("email", logs.MaskEmail(req.Email)),
zap.Int64("admin_user_id", adminUserID),
)
existing, findErr := s.userDAO.FindByUsername(ctx, req.Username)
@@ -192,7 +272,28 @@ func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreate
return nil, ErrUserExists
}
// 加密密码
roleCode := req.RoleCode
if roleCode == "" {
roleCode = constants.RoleUser
}
role, roleErr := s.roleDAO.FindByCode(ctx, roleCode)
if roleErr != nil {
return nil, ErrInvalidRole
}
adminLevel, err := s.roleDAO.GetUserMaxLevel(ctx, adminUserID)
if err != nil {
return nil, err
}
if role.Level <= adminLevel {
logs.Warn(ctx, funcName, "尝试为新用户分配高于自身等级的角色",
zap.String("role_code", roleCode),
zap.Int("role_level", role.Level),
zap.Int("admin_level", adminLevel),
)
return nil, ErrCannotAssignHigherRole
}
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
return nil, err
@@ -214,35 +315,34 @@ func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreate
return nil, err
}
roleCode := req.RoleCode
if roleCode == "" {
roleCode = constants.RoleUser
}
role, roleErr := s.roleDAO.FindByCode(ctx, roleCode)
if roleErr != nil {
logs.Warn(ctx, funcName, "角色查找失败,将跳过角色分配",
zap.String("role_code", roleCode), zap.Error(roleErr))
} else {
if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil {
logs.Warn(ctx, funcName, "角色分配失败",
zap.Int64("user_id", user.ID), zap.Error(assignErr))
}
}
roles, rolesErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
userRoles, rolesErr := s.roleDAO.GetUserRoles(ctx, user.ID)
if rolesErr != nil {
logs.Warn(ctx, funcName, "获取新建用户角色失败",
zap.Int64("user_id", user.ID), zap.Error(rolesErr))
roles = []string{}
userRoles = []model.Role{}
}
logs.Info(ctx, funcName, "管理员创建用户成功", zap.Int64("user_id", user.ID))
return s.buildAdminUserInfo(user, roles), nil
return s.buildAdminUserInfo(user, userRoles), nil
}
// buildAdminUserInfo 从 model.User 构建 dto.AdminUserInfo
func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string) *dto.AdminUserInfo {
if roles == nil {
roles = []string{}
// buildAdminUserInfo 从 model.User + []model.Role 构建 dto.AdminUserInfo
func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []model.Role) *dto.AdminUserInfo {
roleInfos := make([]dto.RoleInfo, 0, len(roles))
minLevel := math.MaxInt32
for _, r := range roles {
roleInfos = append(roleInfos, dto.RoleInfo{
Code: r.Code,
Name: r.Name,
Level: r.Level,
})
if r.Level < minLevel {
minLevel = r.Level
}
}
info := &dto.AdminUserInfo{
@@ -254,7 +354,8 @@ func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string)
Gender: user.Gender,
Status: user.Status,
StatusText: constants.UserStatusMap[user.Status],
Roles: roles,
Roles: roleInfos,
MaxLevel: minLevel,
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
}

View File

@@ -2,6 +2,7 @@ package dao
import (
"context"
"math"
"github.com/echochat/backend/app/auth/model"
"github.com/echochat/backend/pkg/logs"
@@ -117,3 +118,87 @@ func (d *RoleDAO) RemoveUserRoles(ctx context.Context, userID int64) error {
Where("user_id = ?", userID).
Delete(&model.UserRole{}).Error
}
// GetAllRoles 获取所有角色列表(按 level 升序排列)
func (d *RoleDAO) GetAllRoles(ctx context.Context) ([]model.Role, error) {
funcName := "dao.role_dao.GetAllRoles"
logs.Debug(ctx, funcName, "获取所有角色列表")
var roles []model.Role
err := d.db.WithContext(ctx).Order("level ASC").Find(&roles).Error
if err != nil {
logs.Error(ctx, funcName, "获取所有角色失败", zap.Error(err))
return nil, err
}
return roles, nil
}
// GetUserMaxLevel 获取用户的最高权限等级(最小 level 值)
// 如果用户无角色,返回 math.MaxInt32 表示无权限
func (d *RoleDAO) GetUserMaxLevel(ctx context.Context, userID int64) (int, error) {
funcName := "dao.role_dao.GetUserMaxLevel"
logs.Debug(ctx, funcName, "获取用户最高权限等级", zap.Int64("user_id", userID))
var minLevel *int
err := d.db.WithContext(ctx).
Model(&model.UserRole{}).
Joins("JOIN auth_roles ON auth_roles.id = auth_user_roles.role_id").
Where("auth_user_roles.user_id = ?", userID).
Select("MIN(auth_roles.level)").
Scan(&minLevel).Error
if err != nil {
logs.Error(ctx, funcName, "获取用户权限等级失败", zap.Error(err))
return 0, err
}
if minLevel == nil {
return math.MaxInt32, nil
}
return *minLevel, nil
}
// SetUserRoles 事务内先清除再批量设置用户角色
func (d *RoleDAO) SetUserRoles(ctx context.Context, userID int64, roleIDs []int) error {
funcName := "dao.role_dao.SetUserRoles"
logs.Info(ctx, funcName, "设置用户角色",
zap.Int64("user_id", userID),
zap.Ints("role_ids", roleIDs),
)
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", userID).Delete(&model.UserRole{}).Error; err != nil {
logs.Error(ctx, funcName, "清除用户角色失败", zap.Error(err))
return err
}
if len(roleIDs) == 0 {
return nil
}
userRoles := make([]model.UserRole, 0, len(roleIDs))
for _, rid := range roleIDs {
userRoles = append(userRoles, model.UserRole{
UserID: userID,
RoleID: rid,
})
}
if err := tx.Create(&userRoles).Error; err != nil {
logs.Error(ctx, funcName, "批量设置用户角色失败", zap.Error(err))
return err
}
return nil
})
}
// FindByCodeList 按角色代码列表批量查询角色
func (d *RoleDAO) FindByCodeList(ctx context.Context, codes []string) ([]model.Role, error) {
funcName := "dao.role_dao.FindByCodeList"
logs.Debug(ctx, funcName, "批量查询角色", zap.Strings("codes", codes))
var roles []model.Role
err := d.db.WithContext(ctx).Where("code IN ?", codes).Find(&roles).Error
if err != nil {
logs.Error(ctx, funcName, "批量查询角色失败", zap.Error(err))
return nil, err
}
return roles, nil
}

View File

@@ -8,6 +8,7 @@ type Role struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin对应 constants.RoleCode*
Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」
Level int `json:"level" gorm:"not null;default:100"` // 角色等级值越小权限越高1=超管, 10=管理员, 100=普通用户
Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒
}

View File

@@ -26,21 +26,29 @@ type AdminUserInfo struct {
Phone string `json:"phone,omitempty"` // 手机号
Status int `json:"status"` // 账号状态
StatusText string `json:"status_text"` // 状态中文描述
Roles []string `json:"roles"` // 角色代码列表
Roles []RoleInfo `json:"roles"` // 角色详情列表(含 code/name/level
MaxLevel int `json:"max_level"` // 用户最高权限等级(最小 level 值)
LastLoginAt string `json:"last_login_at,omitempty"` // 最后登录时间
LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP
CreatedAt string `json:"created_at"` // 注册时间
UpdatedAt string `json:"updated_at"` // 更新时间
}
// RoleInfo 角色信息(用于前端角色列表展示和角色分配)
type RoleInfo struct {
Code string `json:"code"` // 角色代码
Name string `json:"name"` // 角色中文名称
Level int `json:"level"` // 角色等级,值越小权限越高
}
// UpdateUserStatusRequest 更新用户状态请求
type UpdateUserStatusRequest struct {
Status int `json:"status" binding:"required,oneof=1 2"` // 目标状态1=正常, 2=禁用
}
// AssignRoleRequest 分配角色请求
type AssignRoleRequest struct {
RoleCode string `json:"role_code" binding:"required"` // 角色代码user/admin/super_admin
// SetRolesRequest 批量设置用户角色请求(替换原有的单角色分配)
type SetRolesRequest struct {
RoleCodes []string `json:"role_codes" binding:"required,min=1"` // 角色代码列表
}
// AdminCreateUserRequest 管理员手动创建用户请求

View File

@@ -46,6 +46,7 @@ CREATE TABLE auth_roles (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL,
level INT NOT NULL DEFAULT 100,
description VARCHAR(200) DEFAULT '',
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW()
);
@@ -54,14 +55,15 @@ COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有
COMMENT ON COLUMN auth_roles.id IS '角色唯一标识,自增主键';
COMMENT ON COLUMN auth_roles.code IS '角色代码唯一标识user=普通用户admin=管理员super_admin=超级管理员';
COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"';
COMMENT ON COLUMN auth_roles.level IS '角色等级值越小权限越高1=超管, 10=管理员, 100=普通用户,预留间隔便于扩展';
COMMENT ON COLUMN auth_roles.description IS '角色描述说明';
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
-- 预置角色数据
INSERT INTO auth_roles (code, name, description) VALUES
('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'),
('admin', '管理员', '后台管理员,可以管理用户、监控会议等'),
('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置');
-- 预置角色数据level 值越小权限越高)
INSERT INTO auth_roles (code, name, level, description) VALUES
('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'),
('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'),
('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置');
-- ============================================================
-- auth_user_roles: 用户角色关联表

View File

@@ -7,7 +7,7 @@
---
**Project:** EchoChat
**Generated:** 2026-03-02 10:27:26
**Generated:** 2026-03-02 15:16:13
**Category:** Analytics Dashboard
---
@@ -163,10 +163,14 @@
### Page Pattern
**Pattern Name:** Data-Dense + Drill-Down
**Pattern Name:** Horizontal Scroll Journey
- **CTA Placement:** Above fold
- **Section Order:** Hero > Features > CTA
- **Conversion Strategy:** Immersive product discovery. High engagement. Keep navigation visible.
28,Bento Grid Showcase,bento, grid, features, modular, apple-style, showcase", 1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA, Floating Action Button or Bottom of Grid, Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark., Hover card scale (1.02), video inside cards, tilt effect, staggered reveal, Scannable value props. High information density without clutter. Mobile stack.
29,Interactive 3D Configurator,3d, configurator, customizer, interactive, product", 1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase, Inside Configurator UI + Sticky Bottom Bar, Neutral studio background. Product: Realistic materials. UI: Minimal overlay., Real-time rendering, material swap animation, camera rotate/zoom, light reflection, Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart.
30,AI-Driven Dynamic Landing,ai, dynamic, personalized, adaptive, generative", 1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop, Input Field (Hero) + 'Try it' Buttons, Adaptive to user input. Dark mode for compute feel. Neon accents., Typing text effects, shimmering generation loaders, morphing layouts, Immediate value demonstration. 'Show, don't tell'. Low friction start.
- **CTA Placement:** Floating Sticky CTA or End of Horizontal Track
- **Section Order:** 1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer
---

View File

@@ -0,0 +1,52 @@
# Admin User Detail Page Overrides
> **PROJECT:** EchoChat
> **Generated:** 2026-03-02 15:16:13
> **Page Type:** Dashboard / Data View
> ⚠️ **IMPORTANT:** Rules in this file **override** the Master file (`design-system/MASTER.md`).
> Only deviations from the Master are documented here. For all other rules, refer to the Master.
---
## Page-Specific Rules
### Layout Overrides
- **Max Width:** 1200px (standard)
- **Layout:** Full-width sections, centered content
- **Sections:** 1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer
### Spacing Overrides
- No overrides — use Master spacing
### Typography Overrides
- No overrides — use Master typography
### Color Overrides
- **Strategy:** Continuous palette transition. Chapter colors. Progress bar #000000.
### Component Overrides
- Avoid: Use for flat single-level sites
- Avoid: Ignore accessibility motion settings
- Avoid: Validate only on submit
---
## Page-Specific Components
- No unique components for this page
---
## Recommendations
- Effects: Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill
- Navigation: Use for sites with 3+ levels of depth
- Animation: Check prefers-reduced-motion media query
- Forms: Validate on blur for most fields
- CTA Placement: Floating Sticky CTA or End of Horizontal Track

View File

@@ -11,9 +11,10 @@
|------|------|------|------|
| GET | /api/v1/admin/users | admin | 获取用户列表 |
| GET | /api/v1/admin/users/:id | admin | 获取用户详情 |
| PUT | /api/v1/admin/users/:id/status | admin | 更新用户状态 |
| PUT | /api/v1/admin/users/:id/role | admin / super_admin | 分配用户角色 |
| PUT | /api/v1/admin/users/:id/status | admin | 更新用户状态(受角色等级约束) |
| PUT | /api/v1/admin/users/:id/roles | admin / super_admin | 批量设置用户角色(受角色等级约束) |
| POST | /api/v1/admin/users | admin | 管理员创建用户 |
| GET | /api/v1/admin/roles | admin | 获取所有角色列表(含 level |
| GET | /api/v1/admin/users/:id/meetings | admin | 获取用户会议记录Phase 3 实现) |
---
@@ -51,7 +52,10 @@
"phone": "13800138000",
"status": 1,
"status_text": "正常",
"roles": ["user"],
"roles": [
{ "code": "user", "name": "普通用户", "level": 100 }
],
"max_level": 100,
"last_login_at": "2026-02-27 10:00:00",
"last_login_ip": "192.168.1.100",
"created_at": "2026-02-20 08:00:00",
@@ -85,7 +89,10 @@
"phone": "13800138000",
"status": 1,
"status_text": "正常",
"roles": ["user"],
"roles": [
{ "code": "user", "name": "普通用户", "level": 100 }
],
"max_level": 100,
"last_login_at": "2026-02-27 10:00:00",
"last_login_ip": "192.168.1.100",
"created_at": "2026-02-20 08:00:00",
@@ -111,6 +118,7 @@
**说明:**
- 禁用用户后,该用户的所有活跃 Token 将被清除
- 不能禁用自己的账号(返回 400
- 操作者权限等级必须高于目标用户level 数值更小),否则返回 403
- 禁用后用户尝试登录将返回 403
**成功响应:**
@@ -125,9 +133,9 @@
---
## 4. 分配用户角色
## 4. 批量设置用户角色
`PUT /api/v1/admin/users/:id/role`
`PUT /api/v1/admin/users/:id/roles`
**权限:** admin / super_admin
@@ -135,11 +143,49 @@
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| role_code | string | 是 | 角色代码user / admin / super_admin |
| role_codes | string[] | 是 | 角色代码列表,如 `["user", "admin"]` |
**权限管控规则:**
- 操作者权限等级level必须严格高于目标用户数值更小否则返回 403
- 不能分配等级高于或等于操作者自身的角色,否则返回 403
- 角色列表为全量覆盖(先清除再设置)
- 注:`super_admin` 角色level=1不可通过 API 分配(任何人的 level 都 >= 1超管只能通过数据库直接创建
**成功响应:**
```json
{
"code": 0,
"message": "success"
}
```
**错误响应:**
- 403`权限不足,无法操作更高等级的用户``不能分配高于自身等级的角色`
---
## 5. 管理员创建用户
## 5. 获取所有角色列表
`GET /api/v1/admin/roles`
**权限:** admin
**成功响应:**
```json
{
"code": 0,
"message": "success",
"data": [
{ "code": "super_admin", "name": "超级管理员", "level": 1 },
{ "code": "admin", "name": "管理员", "level": 10 },
{ "code": "user", "name": "普通用户", "level": 100 }
]
}
```
---
## 6. 管理员创建用户
`POST /api/v1/admin/users`
@@ -155,6 +201,14 @@
| nickname | string | 否 | 昵称(默认使用用户名) |
| role_code | string | 否 | 指定角色,默认为 user |
**权限管控规则:**
- 不能为新用户分配等级高于或等于操作者自身的角色,否则返回 403
-adminlevel=10只能分配 level > 10 的角色(如 user不能分配 admin 或 super_admin
**错误响应:**
- 400`用户名或邮箱已被注册` / `无效的角色代码`
- 403`不能分配高于自身等级的角色`
**成功响应:**
```json
{
@@ -169,7 +223,10 @@
"gender": 0,
"status": 1,
"status_text": "正常",
"roles": ["user"],
"roles": [
{ "code": "user", "name": "普通用户", "level": 100 }
],
"max_level": 100,
"created_at": "2026-03-02 11:12:33",
"updated_at": "2026-03-02 11:12:33"
},
@@ -180,7 +237,7 @@
---
## 6. 获取用户会议记录
## 7. 获取用户会议记录
`GET /api/v1/admin/users/:id/meetings`

View File

@@ -265,3 +265,50 @@ JWT Token 的 Claims 中包含 `client_type` 字段,用于:
- [ ] JWT Claims 中包含 client_type 字段
- [ ] 登出时只删除对应 clientType 的 Token
- [ ] Token 刷新时保持原 clientType 不变
---
## 7. 角色等级与权限管控规范
### 7.1 角色等级设计
`auth_roles``level` 字段,值越小权限越高,预留间隔:
| 角色 | Code | Level | 说明 |
|------|------|-------|------|
| 超级管理员 | super_admin | 1 | 最高权限 |
| 管理员 | admin | 10 | 后台管理 |
| 普通用户 | user | 100 | 基础权限 |
用户可拥有多个角色,取**最小 level 值**作为有效权限等级。
### 7.2 权限管控规则
**核心原则:操作者的 level 必须严格小于目标用户的 level才能执行管理操作。**
1. **更新用户状态(禁用/启用)**:操作者 level < 目标用户 level
2. **设置用户角色**操作者 level < 目标用户 level且不能分配 level <= 自身的角色
3. **前端管控**禁用/启用按钮对高等级用户隐藏高等级角色 checkbox 禁用
### 7.3 API 响应变更
`AdminUserInfo` `roles` 字段从 `[]string` 改为 `[]RoleInfo`
```json
{
"roles": [
{ "code": "admin", "name": "管理员", "level": 10 },
{ "code": "user", "name": "普通用户", "level": 100 }
],
"max_level": 10
}
```
### 7.4 检查清单
涉及用户管理操作时必须确认
- [ ] 后端 Service 层调用 `checkPermissionLevel` 进行等级校验
- [ ] 权限不足时返回 403`ErrInsufficientPermission`
- [ ] 前端通过比较 `adminMaxLevel` `targetMaxLevel` 控制 UI 可见性
- [ ] 角色分配使用全量覆盖模式`SetUserRoles`非追加模式

View File

@@ -81,6 +81,8 @@
- 前台用户和后台管理员共用 `auth_users`
- 通过 `auth_roles``auth_user_roles` 区分角色user / admin / super_admin
- `auth_roles.level` 字段实现角色等级层级管控值越小权限越高1=超管, 10=管理员, 100=用户)
- 管理操作强制执行"高等级管理低等级"规则(操作者 level 必须 < 目标用户 level
- 前台 API 路由`/api/v1/auth/*` 仅需 JWT 验证
- 后台 API 路由`/api/v1/admin/*` JWT 验证 + 角色检查双重中间件
@@ -235,6 +237,7 @@ CREATE TABLE auth_roles (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(50) NOT NULL,
level INT NOT NULL DEFAULT 100,
description VARCHAR(200) DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
@@ -243,14 +246,15 @@ COMMENT ON TABLE auth_roles IS '角色表,定义系统中所有
COMMENT ON COLUMN auth_roles.id IS '角色唯一标识,自增主键';
COMMENT ON COLUMN auth_roles.code IS '角色代码唯一标识user=普通用户admin=管理员super_admin=超级管理员';
COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"';
COMMENT ON COLUMN auth_roles.level IS '角色等级值越小权限越高1=超管, 10=管理员, 100=普通用户,预留间隔便于扩展';
COMMENT ON COLUMN auth_roles.description IS '角色描述说明';
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
-- 预置角色数据
INSERT INTO auth_roles (code, name, description) VALUES
('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'),
('admin', '管理员', '后台管理员,可以管理用户、监控会议等'),
('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置');
-- 预置角色数据level 值越小权限越高)
INSERT INTO auth_roles (code, name, level, description) VALUES
('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'),
('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'),
('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置');
-- ============================================================
-- auth_user_roles: 用户角色关联表
@@ -265,8 +269,8 @@ CREATE TABLE auth_user_roles (
#### 权限扩展规划(后期迭代)
> 当前 MVP 阶段使用粗粒度 3 角色(user / admin / super_admin)即可满足需求
> 后期管理员增多、需要区分职责时,引入细粒度权限点机制,扩展如下两张表:
> 当前已实现:3 角色 + level 等级层级管控(super_admin=1, admin=10, user=100),所有管理操作受层级约束
> 后期管理员增多、需要区分职责时,引入细粒度权限点机制,扩展如下两张表level 字段天然支持排序和层级融合)
```sql
-- 预留:权限点表(后期扩展)
@@ -714,8 +718,9 @@ POST /api/v1/admin/auth/login
GET /api/v1/admin/users
GET /api/v1/admin/users/:id
PUT /api/v1/admin/users/:id/status
PUT /api/v1/admin/users/:id/role
PUT /api/v1/admin/users/:id/roles
POST /api/v1/admin/users
GET /api/v1/admin/roles
GET /api/v1/admin/users/:id/meetings
# 会议管理

View File

@@ -3,6 +3,7 @@
> **状态:✅ Phase 1 全部完成**2026-03-02
> 11 个 Task 全部通过端到端验证和代码审查。
> 分支:`feature/phase1-foundation-and-auth`
> **补充:角色等级体系与权限管控已实施**2026-03-02— `auth_roles.level` 字段 + 层级权限 + 多选角色分配
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

View File

@@ -1,6 +1,6 @@
# EchoChat 项目开发进度
> **最后更新**2026-03-02前后端错误处理规范统一 + 安全加固
> **最后更新**2026-03-02角色等级体系与权限管控实施
> **当前阶段**Phase 1 - 基础设施与用户认证
> **当前分支**`feature/phase1-foundation-and-auth`
> **实施计划**`docs/plans/2026-02-27-phase1-foundation-and-auth.md`
@@ -35,6 +35,7 @@
5. **API 响应格式**:统一 `{ "code": 0, "message": "success", "data": ... }`
6. **常量命名**Go camelCase`UserStatusActive`),非大写下划线
7. **模块路由**:模块内自注册 + 中央 router 聚合
8. **角色等级体系**`auth_roles.level` 字段值越小权限越高1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行"高等级管理低等级"规则
### 前台用户端frontend/
1. **框架**uni-app 3.0Vue 3.4.21 框架锁定)
@@ -151,7 +152,7 @@ cd frontend && npm run dev:h5
### 可测试功能
- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用 → 分配角色 → 创建用户
- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用(受角色等级约束) → 批量设置角色Checkbox 多选 + 等级管控) → 创建用户
- **前台 H5**:注册 → 登录 → 个人中心 → 修改资料 → 退出登录
- **API**`GET http://localhost:8085/health`(健康检查)