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:
@@ -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 流程控制开发节奏
|
2. **工作流**:使用 superpowers 流程控制开发节奏
|
||||||
3. **两端差异**:`frontend/` 和 `admin/` 是完全独立的项目,技术栈不需要统一
|
3. **两端差异**:`frontend/` 和 `admin/` 是完全独立的项目,技术栈不需要统一
|
||||||
4. **模块系统**:前端统一使用 ESM(`export`/`import`),禁止 CommonJS
|
4. **模块系统**:前端统一使用 ESM(`export`/`import`),禁止 CommonJS
|
||||||
5. **Go 常量命名**:camelCase(`UserStatusActive`),非大写下划线
|
5. **Go 常量命名**:camelCase(`UserStatusActive`),非大写下划线
|
||||||
6. **API 响应**:统一 `{ "code": 0, "message": "success", "data": ... }`
|
6. **API 响应**:统一 `{ "code": 0, "message": "success", "data": ... }`
|
||||||
7. **JWT 策略**:有状态 JWT,Token 存 Redis(按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响
|
7. **JWT 策略**:有状态 JWT,Token 存 Redis(按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响
|
||||||
8. **代码注释**:所有公开函数、组件、Store 必须有详细注释
|
8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行层级权限校验
|
||||||
|
9. **代码注释**:所有公开函数、组件、Store 必须有详细注释
|
||||||
9. **文档同步**:代码变更后必须同步更新 docs/ 下相关文档
|
9. **文档同步**:代码变更后必须同步更新 docs/ 下相关文档
|
||||||
10. **验证方式**:使用 Playwright MCP 进行页面自动化验证
|
10. **验证方式**:使用 Playwright MCP 进行页面自动化验证
|
||||||
11. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性
|
11. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* 用户管理 API 模块
|
* 用户管理 API 模块
|
||||||
*
|
*
|
||||||
* 对应后端路由:/api/v1/admin/users
|
* 对应后端路由:/api/v1/admin/users, /api/v1/admin/roles
|
||||||
* 所有接口需要 JWT + admin 角色权限
|
* 所有接口需要 JWT + admin 角色权限
|
||||||
*
|
*
|
||||||
* 接口列表:
|
* 接口列表:
|
||||||
* - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选)
|
* - GET /api/v1/admin/users 用户列表(分页 + 搜索 + 筛选)
|
||||||
* - GET /api/v1/admin/users/:id 用户详情
|
* - GET /api/v1/admin/users/:id 用户详情
|
||||||
* - PUT /api/v1/admin/users/:id/status 更新用户状态
|
* - 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 管理员创建用户
|
* - POST /api/v1/admin/users 管理员创建用户
|
||||||
|
* - GET /api/v1/admin/roles 获取所有角色列表
|
||||||
*/
|
*/
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
@@ -57,16 +58,27 @@ export function updateUserStatus(id, status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分配角色
|
* 批量设置用户角色(先清后设)
|
||||||
* @param {number} id - 用户 ID
|
* @param {number} id - 用户 ID
|
||||||
* @param {string} roleCode - 角色代码:user/admin/super_admin
|
* @param {string[]} roleCodes - 角色代码列表
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function assignUserRole(id, roleCode) {
|
export function setUserRoles(id, roleCodes) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/v1/admin/users/${id}/role`,
|
url: `/api/v1/admin/users/${id}/roles`,
|
||||||
method: 'put',
|
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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
|
|
||||||
功能:
|
功能:
|
||||||
- 基本信息展示(ID、用户名、邮箱、昵称、性别、手机、状态、注册/更新时间)
|
- 基本信息展示(ID、用户名、邮箱、昵称、性别、手机、状态、注册/更新时间)
|
||||||
- 角色管理(查看当前角色 + 分配新角色)
|
- 角色管理(Checkbox Group 多选,权限等级管控)
|
||||||
- 状态切换(启用/禁用)
|
- 状态切换(启用/禁用,受角色层级约束)
|
||||||
- 会议记录 Tab(占位,后续完善)
|
- 会议记录 Tab(占位,后续完善)
|
||||||
|
|
||||||
对应后端 API:
|
对应后端 API:
|
||||||
- GET /api/v1/admin/users/:id
|
- GET /api/v1/admin/users/:id
|
||||||
- PUT /api/v1/admin/users/:id/status
|
- 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>
|
<template>
|
||||||
<div class="user-detail-page" v-loading="loading">
|
<div class="user-detail-page" v-loading="loading">
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
{{ userInfo.status_text }}
|
{{ userInfo.status_text }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-actions">
|
<div class="user-actions" v-if="canManageTarget">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userInfo.status === 1"
|
v-if="userInfo.status === 1"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -59,6 +60,9 @@
|
|||||||
启用账号
|
启用账号
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="user-actions" v-else>
|
||||||
|
<el-tag type="info" size="small">权限不足,无法操作该用户</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详细信息表格 -->
|
<!-- 详细信息表格 -->
|
||||||
@@ -80,45 +84,39 @@
|
|||||||
<el-tab-pane label="角色管理" name="roles">
|
<el-tab-pane label="角色管理" name="roles">
|
||||||
<el-card shadow="never" class="role-card">
|
<el-card shadow="never" class="role-card">
|
||||||
<div class="role-section">
|
<div class="role-section">
|
||||||
<h4 class="section-title">当前角色</h4>
|
<h4 class="section-title">角色分配</h4>
|
||||||
<div class="current-roles">
|
<p class="role-hint" v-if="!canManageTarget" style="margin-bottom: 12px">
|
||||||
<el-tag
|
该用户权限等级高于或等于您,无法修改其角色。
|
||||||
v-for="role in userInfo.roles"
|
</p>
|
||||||
:key="role"
|
<el-checkbox-group v-model="selectedRoleCodes" class="role-checkbox-group">
|
||||||
:type="getRoleTagType(role)"
|
<el-checkbox
|
||||||
size="large"
|
v-for="role in allRoles"
|
||||||
class="role-tag"
|
:key="role.code"
|
||||||
|
:value="role.code"
|
||||||
|
:disabled="isRoleDisabled(role)"
|
||||||
|
class="role-checkbox-item"
|
||||||
>
|
>
|
||||||
{{ getRoleName(role) }}
|
<div class="role-checkbox-label">
|
||||||
</el-tag>
|
<el-tag :type="getRoleTagType(role.code)" size="small">{{ role.name }}</el-tag>
|
||||||
<span v-if="!userInfo.roles || userInfo.roles.length === 0" class="no-role">
|
<span class="role-level-badge">Lv.{{ role.level }}</span>
|
||||||
暂未分配角色
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
<p class="role-hint" style="margin-top: 12px">
|
||||||
|
灰色不可选的角色等级高于或等于您的权限,无法分配。勾选后点击"保存"生效。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider />
|
<div class="role-actions" v-if="canManageTarget">
|
||||||
|
|
||||||
<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>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="roleLoading"
|
:loading="roleLoading"
|
||||||
:disabled="!selectedRole"
|
:disabled="!hasRoleChanges"
|
||||||
@click="handleAssignRole"
|
@click="handleSaveRoles"
|
||||||
>
|
>
|
||||||
分配角色
|
保存角色
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
<el-button @click="resetRoleSelection">重置</el-button>
|
||||||
<p class="role-hint">
|
|
||||||
分配角色将为该用户添加所选角色权限。已有角色不会受影响。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -143,103 +141,129 @@
|
|||||||
*
|
*
|
||||||
* 数据流:
|
* 数据流:
|
||||||
* 1. 从路由参数获取 userId
|
* 1. 从路由参数获取 userId
|
||||||
* 2. 调用 API 获取用户详情
|
* 2. 并行调用 getUserDetail + getAllRoles 获取用户详情和角色列表
|
||||||
* 3. 支持状态切换和角色分配操作
|
* 3. 通过比较操作者 level 和目标用户 level 控制权限
|
||||||
|
* 4. 角色分配使用 Checkbox Group 多选,高等级角色禁用
|
||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
/** 加载状态 */
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
/** 用户详情数据 */
|
|
||||||
const userInfo = ref(null)
|
const userInfo = ref(null)
|
||||||
|
|
||||||
/** 当前 Tab */
|
|
||||||
const activeTab = ref('info')
|
const activeTab = ref('info')
|
||||||
|
|
||||||
/** 角色分配选中值 */
|
|
||||||
const selectedRole = ref('')
|
|
||||||
|
|
||||||
/** 角色分配加载状态 */
|
|
||||||
const roleLoading = ref(false)
|
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 userId = computed(() => Number(route.params.id))
|
||||||
|
|
||||||
/** 头像占位字母 */
|
|
||||||
const avatarLetter = computed(() => {
|
const avatarLetter = computed(() => {
|
||||||
if (!userInfo.value) return '?'
|
if (!userInfo.value) return '?'
|
||||||
const name = userInfo.value.nickname || userInfo.value.username || '?'
|
const name = userInfo.value.nickname || userInfo.value.username || '?'
|
||||||
return name.charAt(0).toUpperCase()
|
return name.charAt(0).toUpperCase()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/** 当前操作者的最高权限等级(最小 level 值) */
|
||||||
* 获取状态 Tag 类型
|
const adminMaxLevel = computed(() => {
|
||||||
* @param {number} status
|
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 getStatusType = (status) => {
|
||||||
const map = { 1: 'success', 2: 'danger', 3: 'info' }
|
const map = { 1: 'success', 2: 'danger', 3: 'info' }
|
||||||
return map[status] || 'info'
|
return map[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取性别文本
|
|
||||||
* @param {number} gender
|
|
||||||
*/
|
|
||||||
const getGenderText = (gender) => {
|
const getGenderText = (gender) => {
|
||||||
const map = { 0: '未知', 1: '男', 2: '女' }
|
const map = { 0: '未知', 1: '男', 2: '女' }
|
||||||
return map[gender] || '未知'
|
return map[gender] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取角色 Tag 类型
|
|
||||||
* @param {string} role
|
|
||||||
*/
|
|
||||||
const getRoleTagType = (role) => {
|
const getRoleTagType = (role) => {
|
||||||
const map = { super_admin: 'danger', admin: 'warning', user: '' }
|
const map = { super_admin: 'danger', admin: 'warning', user: '' }
|
||||||
return map[role] || 'info'
|
return map[role] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取角色中文名称
|
* 判断角色 checkbox 是否禁用
|
||||||
* @param {string} role
|
* 规则:角色 level <= 操作者 level 时禁用(不能分配高于或等于自身等级的角色)
|
||||||
|
* 如果无法管理目标用户,全部禁用
|
||||||
*/
|
*/
|
||||||
const getRoleName = (role) => {
|
const isRoleDisabled = (role) => {
|
||||||
const map = { super_admin: '超级管理员', admin: '管理员', user: '普通用户' }
|
if (!canManageTarget.value) return true
|
||||||
return map[role] || role
|
return role.level <= adminMaxLevel.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 重置角色选择到原始状态 */
|
||||||
* 获取用户详情
|
const resetRoleSelection = () => {
|
||||||
*/
|
selectedRoleCodes.value = [...originalRoleCodes.value]
|
||||||
const fetchUserDetail = async () => {
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getUserDetail(userId.value)
|
const [userRes, rolesRes] = await Promise.all([
|
||||||
userInfo.value = res.data
|
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) {
|
} catch (err) {
|
||||||
console.error('获取用户详情失败:', err)
|
console.error('获取数据失败:', err)
|
||||||
userInfo.value = null
|
userInfo.value = null
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 返回列表 */
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.push('/user/list')
|
router.push('/user/list')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换用户状态
|
|
||||||
* @param {number} newStatus
|
|
||||||
*/
|
|
||||||
const handleToggleStatus = async (newStatus) => {
|
const handleToggleStatus = async (newStatus) => {
|
||||||
const action = newStatus === 2 ? '禁用' : '启用'
|
const action = newStatus === 2 ? '禁用' : '启用'
|
||||||
try {
|
try {
|
||||||
@@ -248,38 +272,33 @@ const handleToggleStatus = async (newStatus) => {
|
|||||||
`${action}确认`,
|
`${action}确认`,
|
||||||
{ type: 'warning' }
|
{ type: 'warning' }
|
||||||
)
|
)
|
||||||
|
|
||||||
await updateUserStatus(userId.value, newStatus)
|
await updateUserStatus(userId.value, newStatus)
|
||||||
ElMessage.success(`${action}成功`)
|
ElMessage.success(`${action}成功`)
|
||||||
fetchUserDetail()
|
fetchData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err !== 'cancel') {
|
if (err !== 'cancel' && err !== 'close') {
|
||||||
console.error(`${action}失败:`, err)
|
console.error(`${action}失败:`, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const handleSaveRoles = async () => {
|
||||||
* 分配角色
|
if (!hasRoleChanges.value) return
|
||||||
*/
|
|
||||||
const handleAssignRole = async () => {
|
|
||||||
if (!selectedRole.value) return
|
|
||||||
|
|
||||||
roleLoading.value = true
|
roleLoading.value = true
|
||||||
try {
|
try {
|
||||||
await assignUserRole(userId.value, selectedRole.value)
|
await setUserRoles(userId.value, selectedRoleCodes.value)
|
||||||
ElMessage.success('角色分配成功')
|
ElMessage.success('角色设置成功')
|
||||||
selectedRole.value = ''
|
fetchData()
|
||||||
fetchUserDetail()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('角色分配失败:', err)
|
console.error('角色设置失败:', err)
|
||||||
} finally {
|
} finally {
|
||||||
roleLoading.value = false
|
roleLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUserDetail()
|
fetchData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -369,7 +388,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.role-section {
|
.role-section {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
@@ -379,27 +398,37 @@ onMounted(() => {
|
|||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-roles {
|
.role-checkbox-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-tag {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-role {
|
|
||||||
color: #94A3B8;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assign-role-form {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
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 {
|
.role-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #94A3B8;
|
color: #94A3B8;
|
||||||
|
|||||||
@@ -89,12 +89,12 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag
|
<el-tag
|
||||||
v-for="role in row.roles"
|
v-for="role in row.roles"
|
||||||
:key="role"
|
:key="role.code"
|
||||||
:type="getRoleTagType(role)"
|
:type="getRoleTagType(role.code)"
|
||||||
size="small"
|
size="small"
|
||||||
class="role-tag"
|
class="role-tag"
|
||||||
>
|
>
|
||||||
{{ getRoleName(role) }}
|
{{ role.name }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-if="!row.roles || row.roles.length === 0" class="no-role">未分配</span>
|
<span v-if="!row.roles || row.roles.length === 0" class="no-role">未分配</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -121,6 +121,7 @@
|
|||||||
<el-button type="primary" link size="small" @click.stop="goDetail(row.id)">
|
<el-button type="primary" link size="small" @click.stop="goDetail(row.id)">
|
||||||
详情
|
详情
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<template v-if="canManageRow(row)">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.status === 1"
|
v-if="row.status === 1"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -140,6 +141,7 @@
|
|||||||
启用
|
启用
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
@@ -185,8 +187,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色" prop="role_code">
|
<el-form-item label="角色" prop="role_code">
|
||||||
<el-select v-model="createForm.role_code" placeholder="选择角色" style="width: 100%">
|
<el-select v-model="createForm.role_code" placeholder="选择角色" style="width: 100%">
|
||||||
<el-option label="普通用户" value="user" />
|
<el-option
|
||||||
<el-option label="管理员" value="admin" />
|
v-for="role in assignableRoles"
|
||||||
|
:key="role.code"
|
||||||
|
:label="role.name"
|
||||||
|
:value="role.code"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -207,12 +213,14 @@
|
|||||||
* 2. 搜索/筛选/分页变化时重新加载
|
* 2. 搜索/筛选/分页变化时重新加载
|
||||||
* 3. 操作(禁用/启用/创建)成功后刷新列表
|
* 3. 操作(禁用/启用/创建)成功后刷新列表
|
||||||
*/
|
*/
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
/** 加载状态 */
|
/** 加载状态 */
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -233,6 +241,36 @@ const pagination = reactive({
|
|||||||
total: 0
|
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)
|
const showCreateDialog = ref(false)
|
||||||
|
|
||||||
@@ -276,15 +314,6 @@ const getRoleTagType = (role) => {
|
|||||||
return map[role] || 'info'
|
return map[role] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取角色中文名称
|
|
||||||
* @param {string} role - 角色代码
|
|
||||||
*/
|
|
||||||
const getRoleName = (role) => {
|
|
||||||
const map = { super_admin: '超级管理员', admin: '管理员', user: '普通用户' }
|
|
||||||
return map[role] || role
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取状态 Tag 类型
|
* 获取状态 Tag 类型
|
||||||
* @param {number} status - 状态码
|
* @param {number} status - 状态码
|
||||||
@@ -364,7 +393,7 @@ const handleToggleStatus = async (row, newStatus) => {
|
|||||||
ElMessage.success(`${action}成功`)
|
ElMessage.success(`${action}成功`)
|
||||||
fetchUserList()
|
fetchUserList()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err !== 'cancel') {
|
if (err !== 'cancel' && err !== 'close') {
|
||||||
console.error(`${action}用户失败:`, err)
|
console.error(`${action}用户失败:`, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,8 +434,19 @@ const resetCreateForm = () => {
|
|||||||
createForm.role_code = 'user'
|
createForm.role_code = 'user'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取角色列表(用于权限判断和创建用户的角色选项) */
|
||||||
|
const fetchRoles = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getAllRoles()
|
||||||
|
allRoles.value = res.data || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取角色列表失败:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchUserList()
|
fetchUserList()
|
||||||
|
fetchRoles()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) {
|
|||||||
utils.ResponseBadRequest(c, "不能禁用自己的账号")
|
utils.ResponseBadRequest(c, "不能禁用自己的账号")
|
||||||
case service.ErrInvalidStatus:
|
case service.ErrInvalidStatus:
|
||||||
utils.ResponseBadRequest(c, "无效的用户状态")
|
utils.ResponseBadRequest(c, "无效的用户状态")
|
||||||
|
case service.ErrInsufficientPermission:
|
||||||
|
utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户")
|
||||||
default:
|
default:
|
||||||
logs.Error(ctx, funcName, "更新用户状态失败", zap.Error(err))
|
logs.Error(ctx, funcName, "更新用户状态失败", zap.Error(err))
|
||||||
utils.ResponseError(c, "更新用户状态失败")
|
utils.ResponseError(c, "更新用户状态失败")
|
||||||
@@ -129,10 +131,10 @@ func (ctl *UserManageController) UpdateUserStatus(c *gin.Context) {
|
|||||||
utils.ResponseOK(c, nil)
|
utils.ResponseOK(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignRole 分配角色
|
// SetRoles 批量设置用户角色
|
||||||
// PUT /api/v1/admin/users/:id/role
|
// PUT /api/v1/admin/users/:id/roles
|
||||||
func (ctl *UserManageController) AssignRole(c *gin.Context) {
|
func (ctl *UserManageController) SetRoles(c *gin.Context) {
|
||||||
funcName := "controller.user_manage_controller.AssignRole"
|
funcName := "controller.user_manage_controller.SetRoles"
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
@@ -141,27 +143,38 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req dto.AssignRoleRequest
|
var req dto.SetRolesRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
utils.ResponseBadRequest(c, "参数错误: "+err.Error())
|
utils.ResponseBadRequest(c, "参数错误: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logs.Info(ctx, funcName, "分配角色",
|
adminUserID, ok := middleware.GetCurrentUserID(c)
|
||||||
zap.Int64("user_id", userID),
|
if !ok {
|
||||||
zap.String("role_code", req.RoleCode),
|
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 {
|
switch err {
|
||||||
case service.ErrUserNotFound:
|
case service.ErrUserNotFound:
|
||||||
utils.ResponseNotFound(c, "用户不存在")
|
utils.ResponseNotFound(c, "用户不存在")
|
||||||
case service.ErrInvalidRole:
|
case service.ErrInvalidRole:
|
||||||
utils.ResponseBadRequest(c, "无效的角色代码")
|
utils.ResponseBadRequest(c, "包含无效的角色代码")
|
||||||
|
case service.ErrInsufficientPermission:
|
||||||
|
utils.ResponseForbidden(c, "权限不足,无法操作更高等级的用户")
|
||||||
|
case service.ErrCannotAssignHigherRole:
|
||||||
|
utils.ResponseForbidden(c, "不能分配高于自身等级的角色")
|
||||||
default:
|
default:
|
||||||
logs.Error(ctx, funcName, "分配角色失败", zap.Error(err))
|
logs.Error(ctx, funcName, "设置用户角色失败", zap.Error(err))
|
||||||
utils.ResponseError(c, "分配角色失败")
|
utils.ResponseError(c, "设置用户角色失败")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -169,6 +182,24 @@ func (ctl *UserManageController) AssignRole(c *gin.Context) {
|
|||||||
utils.ResponseOK(c, nil)
|
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 管理员手动创建用户
|
// CreateUser 管理员手动创建用户
|
||||||
// POST /api/v1/admin/users
|
// POST /api/v1/admin/users
|
||||||
func (ctl *UserManageController) CreateUser(c *gin.Context) {
|
func (ctl *UserManageController) CreateUser(c *gin.Context) {
|
||||||
@@ -182,19 +213,31 @@ func (ctl *UserManageController) CreateUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminUserID, ok := middleware.GetCurrentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logs.Info(ctx, funcName, "管理员创建用户",
|
logs.Info(ctx, funcName, "管理员创建用户",
|
||||||
zap.String("username", req.Username),
|
zap.String("username", req.Username),
|
||||||
zap.String("email", logs.MaskEmail(req.Email)),
|
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 != nil {
|
||||||
if err == service.ErrUserExists {
|
switch err {
|
||||||
|
case service.ErrUserExists:
|
||||||
utils.ResponseBadRequest(c, "用户名或邮箱已被注册")
|
utils.ResponseBadRequest(c, "用户名或邮箱已被注册")
|
||||||
return
|
case service.ErrInvalidRole:
|
||||||
}
|
utils.ResponseBadRequest(c, "无效的角色代码")
|
||||||
|
case service.ErrCannotAssignHigherRole:
|
||||||
|
utils.ResponseForbidden(c, "不能分配高于自身等级的角色")
|
||||||
|
default:
|
||||||
logs.Error(ctx, funcName, "创建用户失败", zap.Error(err))
|
logs.Error(ctx, funcName, "创建用户失败", zap.Error(err))
|
||||||
utils.ResponseError(c, "创建用户失败")
|
utils.ResponseError(c, "创建用户失败")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ func RegisterRoutes(
|
|||||||
adminGroup.GET("/users", ctrl.GetUserList)
|
adminGroup.GET("/users", ctrl.GetUserList)
|
||||||
adminGroup.GET("/users/:id", ctrl.GetUserDetail)
|
adminGroup.GET("/users/:id", ctrl.GetUserDetail)
|
||||||
adminGroup.PUT("/users/:id/status", ctrl.UpdateUserStatus)
|
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.POST("/users", ctrl.CreateUser)
|
||||||
|
|
||||||
|
// 角色管理
|
||||||
|
adminGroup.GET("/roles", ctrl.GetAllRoles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
|
|
||||||
authDAO "github.com/echochat/backend/app/auth/dao"
|
authDAO "github.com/echochat/backend/app/auth/dao"
|
||||||
"github.com/echochat/backend/app/auth/model"
|
"github.com/echochat/backend/app/auth/model"
|
||||||
@@ -22,6 +23,8 @@ var (
|
|||||||
ErrInvalidStatus = errors.New("无效的用户状态")
|
ErrInvalidStatus = errors.New("无效的用户状态")
|
||||||
ErrInvalidRole = errors.New("无效的角色代码")
|
ErrInvalidRole = errors.New("无效的角色代码")
|
||||||
ErrCannotDisableSelf = errors.New("不能禁用自己的账号")
|
ErrCannotDisableSelf = errors.New("不能禁用自己的账号")
|
||||||
|
ErrInsufficientPermission = errors.New("权限不足,无法操作更高等级的用户")
|
||||||
|
ErrCannotAssignHigherRole = errors.New("不能分配高于自身等级的角色")
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserManageService 管理端用户管理服务
|
// UserManageService 管理端用户管理服务
|
||||||
@@ -56,11 +59,11 @@ func (s *UserManageService) GetUserList(ctx context.Context, req *dto.UserListRe
|
|||||||
|
|
||||||
list := make([]dto.AdminUserInfo, 0, len(users))
|
list := make([]dto.AdminUserInfo, 0, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
|
||||||
if roleErr != nil {
|
if roleErr != nil {
|
||||||
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
|
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
|
||||||
zap.Int64("user_id", user.ID), zap.Error(roleErr))
|
zap.Int64("user_id", user.ID), zap.Error(roleErr))
|
||||||
roles = []string{}
|
roles = []model.Role{}
|
||||||
}
|
}
|
||||||
list = append(list, *s.buildAdminUserInfo(&user, roles))
|
list = append(list, *s.buildAdminUserInfo(&user, roles))
|
||||||
}
|
}
|
||||||
@@ -84,17 +87,17 @@ func (s *UserManageService) GetUserDetail(ctx context.Context, userID int64) (*d
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
roles, roleErr := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
roles, roleErr := s.roleDAO.GetUserRoles(ctx, user.ID)
|
||||||
if roleErr != nil {
|
if roleErr != nil {
|
||||||
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
|
logs.Warn(ctx, funcName, "获取用户角色失败,降级为空角色列表",
|
||||||
zap.Int64("user_id", user.ID), zap.Error(roleErr))
|
zap.Int64("user_id", user.ID), zap.Error(roleErr))
|
||||||
roles = []string{}
|
roles = []model.Role{}
|
||||||
}
|
}
|
||||||
return s.buildAdminUserInfo(user, roles), nil
|
return s.buildAdminUserInfo(user, roles), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserStatus 启用/禁用用户
|
// UpdateUserStatus 启用/禁用用户
|
||||||
// adminUserID 用于防止管理员禁用自己
|
// adminUserID 用于防止管理员禁用自己;同时校验操作者权限等级必须高于目标用户
|
||||||
func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error {
|
func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64, status int, adminUserID int64) error {
|
||||||
funcName := "service.user_manage_service.UpdateUserStatus"
|
funcName := "service.user_manage_service.UpdateUserStatus"
|
||||||
logs.Info(ctx, funcName, "更新用户状态",
|
logs.Info(ctx, funcName, "更新用户状态",
|
||||||
@@ -111,7 +114,6 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
|
|||||||
return ErrInvalidStatus
|
return ErrInvalidStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认用户存在
|
|
||||||
_, err := s.userDAO.FindByID(ctx, userID)
|
_, err := s.userDAO.FindByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -120,6 +122,10 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil {
|
if err := s.userManageDAO.UpdateUserStatus(ctx, userID, status); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -131,15 +137,16 @@ func (s *UserManageService) UpdateUserStatus(ctx context.Context, userID int64,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignUserRole 分配角色给用户
|
// SetUserRoles 批量设置用户角色(事务内先清后设)
|
||||||
func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, roleCode string) error {
|
// adminUserID 用于校验操作者等级,防止越权分配高等级角色
|
||||||
funcName := "service.user_manage_service.AssignUserRole"
|
func (s *UserManageService) SetUserRoles(ctx context.Context, userID int64, roleCodes []string, adminUserID int64) error {
|
||||||
logs.Info(ctx, funcName, "分配角色",
|
funcName := "service.user_manage_service.SetUserRoles"
|
||||||
zap.Int64("user_id", userID),
|
logs.Info(ctx, funcName, "设置用户角色",
|
||||||
zap.String("role_code", roleCode),
|
zap.Int64("target_user_id", userID),
|
||||||
|
zap.Strings("role_codes", roleCodes),
|
||||||
|
zap.Int64("admin_user_id", adminUserID),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 验证用户存在
|
|
||||||
_, err := s.userDAO.FindByID(ctx, userID)
|
_, err := s.userDAO.FindByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -148,33 +155,106 @@ func (s *UserManageService) AssignUserRole(ctx context.Context, userID int64, ro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找角色
|
if err := s.checkPermissionLevel(ctx, adminUserID, userID); err != nil {
|
||||||
role, err := s.roleDAO.FindByCode(ctx, roleCode)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := s.roleDAO.FindByCodeList(ctx, roleCodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return err
|
||||||
|
}
|
||||||
|
if len(roles) != len(roleCodes) {
|
||||||
return ErrInvalidRole
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分配角色(RoleDAO.AssignRole 内部会处理重复分配)
|
logs.Info(ctx, funcName, "用户角色设置成功",
|
||||||
if err := s.roleDAO.AssignRole(ctx, userID, role.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logs.Info(ctx, funcName, "角色分配成功",
|
|
||||||
zap.Int64("user_id", userID),
|
zap.Int64("user_id", userID),
|
||||||
zap.String("role_code", roleCode),
|
zap.Strings("role_codes", roleCodes),
|
||||||
)
|
)
|
||||||
return nil
|
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 管理员手动创建用户
|
// 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"
|
funcName := "service.user_manage_service.CreateUser"
|
||||||
logs.Info(ctx, funcName, "管理员创建用户",
|
logs.Info(ctx, funcName, "管理员创建用户",
|
||||||
zap.String("username", req.Username),
|
zap.String("username", req.Username),
|
||||||
zap.String("email", logs.MaskEmail(req.Email)),
|
zap.String("email", logs.MaskEmail(req.Email)),
|
||||||
|
zap.Int64("admin_user_id", adminUserID),
|
||||||
)
|
)
|
||||||
|
|
||||||
existing, findErr := s.userDAO.FindByUsername(ctx, req.Username)
|
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
|
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)
|
hashedPassword, err := utils.HashPassword(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -214,35 +315,34 @@ func (s *UserManageService) CreateUser(ctx context.Context, req *dto.AdminCreate
|
|||||||
return nil, err
|
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 {
|
if assignErr := s.roleDAO.AssignRole(ctx, user.ID, role.ID); assignErr != nil {
|
||||||
logs.Warn(ctx, funcName, "角色分配失败",
|
logs.Warn(ctx, funcName, "角色分配失败",
|
||||||
zap.Int64("user_id", user.ID), zap.Error(assignErr))
|
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 {
|
if rolesErr != nil {
|
||||||
logs.Warn(ctx, funcName, "获取新建用户角色失败",
|
logs.Warn(ctx, funcName, "获取新建用户角色失败",
|
||||||
zap.Int64("user_id", user.ID), zap.Error(rolesErr))
|
zap.Int64("user_id", user.ID), zap.Error(rolesErr))
|
||||||
roles = []string{}
|
userRoles = []model.Role{}
|
||||||
}
|
}
|
||||||
logs.Info(ctx, funcName, "管理员创建用户成功", zap.Int64("user_id", user.ID))
|
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
|
// buildAdminUserInfo 从 model.User + []model.Role 构建 dto.AdminUserInfo
|
||||||
func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string) *dto.AdminUserInfo {
|
func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []model.Role) *dto.AdminUserInfo {
|
||||||
if roles == nil {
|
roleInfos := make([]dto.RoleInfo, 0, len(roles))
|
||||||
roles = []string{}
|
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{
|
info := &dto.AdminUserInfo{
|
||||||
@@ -254,7 +354,8 @@ func (s *UserManageService) buildAdminUserInfo(user *model.User, roles []string)
|
|||||||
Gender: user.Gender,
|
Gender: user.Gender,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
StatusText: constants.UserStatusMap[user.Status],
|
StatusText: constants.UserStatusMap[user.Status],
|
||||||
Roles: roles,
|
Roles: roleInfos,
|
||||||
|
MaxLevel: minLevel,
|
||||||
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
|
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package dao
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/echochat/backend/app/auth/model"
|
"github.com/echochat/backend/app/auth/model"
|
||||||
"github.com/echochat/backend/pkg/logs"
|
"github.com/echochat/backend/pkg/logs"
|
||||||
@@ -117,3 +118,87 @@ func (d *RoleDAO) RemoveUserRoles(ctx context.Context, userID int64) error {
|
|||||||
Where("user_id = ?", userID).
|
Where("user_id = ?", userID).
|
||||||
Delete(&model.UserRole{}).Error
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Role struct {
|
|||||||
ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键
|
ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键
|
||||||
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin(对应 constants.RoleCode*)
|
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"` // 角色中文显示名称,如「普通用户」「管理员」
|
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:''"` // 角色描述说明,用于后台管理界面展示
|
Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒
|
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"` // 创建时间,由 GORM 自动填充,精确到秒
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,21 +26,29 @@ type AdminUserInfo struct {
|
|||||||
Phone string `json:"phone,omitempty"` // 手机号
|
Phone string `json:"phone,omitempty"` // 手机号
|
||||||
Status int `json:"status"` // 账号状态
|
Status int `json:"status"` // 账号状态
|
||||||
StatusText string `json:"status_text"` // 状态中文描述
|
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"` // 最后登录时间
|
LastLoginAt string `json:"last_login_at,omitempty"` // 最后登录时间
|
||||||
LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP
|
LastLoginIP string `json:"last_login_ip,omitempty"` // 最后登录 IP
|
||||||
CreatedAt string `json:"created_at"` // 注册时间
|
CreatedAt string `json:"created_at"` // 注册时间
|
||||||
UpdatedAt string `json:"updated_at"` // 更新时间
|
UpdatedAt string `json:"updated_at"` // 更新时间
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoleInfo 角色信息(用于前端角色列表展示和角色分配)
|
||||||
|
type RoleInfo struct {
|
||||||
|
Code string `json:"code"` // 角色代码
|
||||||
|
Name string `json:"name"` // 角色中文名称
|
||||||
|
Level int `json:"level"` // 角色等级,值越小权限越高
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserStatusRequest 更新用户状态请求
|
// UpdateUserStatusRequest 更新用户状态请求
|
||||||
type UpdateUserStatusRequest struct {
|
type UpdateUserStatusRequest struct {
|
||||||
Status int `json:"status" binding:"required,oneof=1 2"` // 目标状态:1=正常, 2=禁用
|
Status int `json:"status" binding:"required,oneof=1 2"` // 目标状态:1=正常, 2=禁用
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignRoleRequest 分配角色请求
|
// SetRolesRequest 批量设置用户角色请求(替换原有的单角色分配)
|
||||||
type AssignRoleRequest struct {
|
type SetRolesRequest struct {
|
||||||
RoleCode string `json:"role_code" binding:"required"` // 角色代码:user/admin/super_admin
|
RoleCodes []string `json:"role_codes" binding:"required,min=1"` // 角色代码列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminCreateUserRequest 管理员手动创建用户请求
|
// AdminCreateUserRequest 管理员手动创建用户请求
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ CREATE TABLE auth_roles (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
code VARCHAR(50) UNIQUE NOT NULL,
|
code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
|
level INT NOT NULL DEFAULT 100,
|
||||||
description VARCHAR(200) DEFAULT '',
|
description VARCHAR(200) DEFAULT '',
|
||||||
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW()
|
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.id IS '角色唯一标识,自增主键';
|
||||||
COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员';
|
COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员';
|
||||||
COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"';
|
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.description IS '角色描述说明';
|
||||||
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
|
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
|
||||||
|
|
||||||
-- 预置角色数据
|
-- 预置角色数据(level 值越小权限越高)
|
||||||
INSERT INTO auth_roles (code, name, description) VALUES
|
INSERT INTO auth_roles (code, name, level, description) VALUES
|
||||||
('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'),
|
('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'),
|
||||||
('admin', '管理员', '后台管理员,可以管理用户、监控会议等'),
|
('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'),
|
||||||
('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置');
|
('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置');
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- auth_user_roles: 用户角色关联表
|
-- auth_user_roles: 用户角色关联表
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Project:** EchoChat
|
**Project:** EchoChat
|
||||||
**Generated:** 2026-03-02 10:27:26
|
**Generated:** 2026-03-02 15:16:13
|
||||||
**Category:** Analytics Dashboard
|
**Category:** Analytics Dashboard
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -163,10 +163,14 @@
|
|||||||
|
|
||||||
### Page Pattern
|
### Page Pattern
|
||||||
|
|
||||||
**Pattern Name:** Data-Dense + Drill-Down
|
**Pattern Name:** Horizontal Scroll Journey
|
||||||
|
|
||||||
- **CTA Placement:** Above fold
|
- **Conversion Strategy:** Immersive product discovery. High engagement. Keep navigation visible.
|
||||||
- **Section Order:** Hero > Features > CTA
|
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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
52
design-system/echochat/pages/admin-user-detail.md
Normal file
52
design-system/echochat/pages/admin-user-detail.md
Normal 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
|
||||||
@@ -11,9 +11,10 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| GET | /api/v1/admin/users | admin | 获取用户列表 |
|
| GET | /api/v1/admin/users | admin | 获取用户列表 |
|
||||||
| GET | /api/v1/admin/users/:id | admin | 获取用户详情 |
|
| GET | /api/v1/admin/users/:id | admin | 获取用户详情 |
|
||||||
| PUT | /api/v1/admin/users/:id/status | 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/roles | admin / super_admin | 批量设置用户角色(受角色等级约束) |
|
||||||
| POST | /api/v1/admin/users | admin | 管理员创建用户 |
|
| POST | /api/v1/admin/users | admin | 管理员创建用户 |
|
||||||
|
| GET | /api/v1/admin/roles | admin | 获取所有角色列表(含 level) |
|
||||||
| GET | /api/v1/admin/users/:id/meetings | admin | 获取用户会议记录(Phase 3 实现) |
|
| GET | /api/v1/admin/users/:id/meetings | admin | 获取用户会议记录(Phase 3 实现) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -51,7 +52,10 @@
|
|||||||
"phone": "13800138000",
|
"phone": "13800138000",
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"status_text": "正常",
|
"status_text": "正常",
|
||||||
"roles": ["user"],
|
"roles": [
|
||||||
|
{ "code": "user", "name": "普通用户", "level": 100 }
|
||||||
|
],
|
||||||
|
"max_level": 100,
|
||||||
"last_login_at": "2026-02-27 10:00:00",
|
"last_login_at": "2026-02-27 10:00:00",
|
||||||
"last_login_ip": "192.168.1.100",
|
"last_login_ip": "192.168.1.100",
|
||||||
"created_at": "2026-02-20 08:00:00",
|
"created_at": "2026-02-20 08:00:00",
|
||||||
@@ -85,7 +89,10 @@
|
|||||||
"phone": "13800138000",
|
"phone": "13800138000",
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"status_text": "正常",
|
"status_text": "正常",
|
||||||
"roles": ["user"],
|
"roles": [
|
||||||
|
{ "code": "user", "name": "普通用户", "level": 100 }
|
||||||
|
],
|
||||||
|
"max_level": 100,
|
||||||
"last_login_at": "2026-02-27 10:00:00",
|
"last_login_at": "2026-02-27 10:00:00",
|
||||||
"last_login_ip": "192.168.1.100",
|
"last_login_ip": "192.168.1.100",
|
||||||
"created_at": "2026-02-20 08:00:00",
|
"created_at": "2026-02-20 08:00:00",
|
||||||
@@ -111,6 +118,7 @@
|
|||||||
**说明:**
|
**说明:**
|
||||||
- 禁用用户后,该用户的所有活跃 Token 将被清除
|
- 禁用用户后,该用户的所有活跃 Token 将被清除
|
||||||
- 不能禁用自己的账号(返回 400)
|
- 不能禁用自己的账号(返回 400)
|
||||||
|
- 操作者权限等级必须高于目标用户(level 数值更小),否则返回 403
|
||||||
- 禁用后用户尝试登录将返回 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
|
**权限:** 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`
|
`POST /api/v1/admin/users`
|
||||||
|
|
||||||
@@ -155,6 +201,14 @@
|
|||||||
| nickname | string | 否 | 昵称(默认使用用户名) |
|
| nickname | string | 否 | 昵称(默认使用用户名) |
|
||||||
| role_code | string | 否 | 指定角色,默认为 user |
|
| role_code | string | 否 | 指定角色,默认为 user |
|
||||||
|
|
||||||
|
**权限管控规则:**
|
||||||
|
- 不能为新用户分配等级高于或等于操作者自身的角色,否则返回 403
|
||||||
|
- 例:admin(level=10)只能分配 level > 10 的角色(如 user),不能分配 admin 或 super_admin
|
||||||
|
|
||||||
|
**错误响应:**
|
||||||
|
- 400:`用户名或邮箱已被注册` / `无效的角色代码`
|
||||||
|
- 403:`不能分配高于自身等级的角色`
|
||||||
|
|
||||||
**成功响应:**
|
**成功响应:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -169,7 +223,10 @@
|
|||||||
"gender": 0,
|
"gender": 0,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"status_text": "正常",
|
"status_text": "正常",
|
||||||
"roles": ["user"],
|
"roles": [
|
||||||
|
{ "code": "user", "name": "普通用户", "level": 100 }
|
||||||
|
],
|
||||||
|
"max_level": 100,
|
||||||
"created_at": "2026-03-02 11:12:33",
|
"created_at": "2026-03-02 11:12:33",
|
||||||
"updated_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`
|
`GET /api/v1/admin/users/:id/meetings`
|
||||||
|
|
||||||
|
|||||||
@@ -265,3 +265,50 @@ JWT Token 的 Claims 中包含 `client_type` 字段,用于:
|
|||||||
- [ ] JWT Claims 中包含 client_type 字段
|
- [ ] JWT Claims 中包含 client_type 字段
|
||||||
- [ ] 登出时只删除对应 clientType 的 Token
|
- [ ] 登出时只删除对应 clientType 的 Token
|
||||||
- [ ] Token 刷新时保持原 clientType 不变
|
- [ ] 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`),非追加模式
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
|
|
||||||
- 前台用户和后台管理员共用 `auth_users` 表
|
- 前台用户和后台管理员共用 `auth_users` 表
|
||||||
- 通过 `auth_roles` 和 `auth_user_roles` 区分角色(user / admin / super_admin)
|
- 通过 `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/auth/*` — 仅需 JWT 验证
|
||||||
- 后台 API 路由:`/api/v1/admin/*` — JWT 验证 + 角色检查双重中间件
|
- 后台 API 路由:`/api/v1/admin/*` — JWT 验证 + 角色检查双重中间件
|
||||||
|
|
||||||
@@ -235,6 +237,7 @@ CREATE TABLE auth_roles (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
code VARCHAR(50) UNIQUE NOT NULL,
|
code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
name VARCHAR(50) NOT NULL,
|
name VARCHAR(50) NOT NULL,
|
||||||
|
level INT NOT NULL DEFAULT 100,
|
||||||
description VARCHAR(200) DEFAULT '',
|
description VARCHAR(200) DEFAULT '',
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
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.id IS '角色唯一标识,自增主键';
|
||||||
COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员';
|
COMMENT ON COLUMN auth_roles.code IS '角色代码,唯一标识:user=普通用户,admin=管理员,super_admin=超级管理员';
|
||||||
COMMENT ON COLUMN auth_roles.name IS '角色显示名称,如"普通用户""管理员""超级管理员"';
|
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.description IS '角色描述说明';
|
||||||
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
|
COMMENT ON COLUMN auth_roles.created_at IS '创建时间';
|
||||||
|
|
||||||
-- 预置角色数据
|
-- 预置角色数据(level 值越小权限越高)
|
||||||
INSERT INTO auth_roles (code, name, description) VALUES
|
INSERT INTO auth_roles (code, name, level, description) VALUES
|
||||||
('user', '普通用户', '系统普通用户,可以使用聊天、会议等功能'),
|
('user', '普通用户', 100, '系统普通用户,可以使用聊天、会议等功能'),
|
||||||
('admin', '管理员', '后台管理员,可以管理用户、监控会议等'),
|
('admin', '管理员', 10, '后台管理员,可以管理用户、监控会议等'),
|
||||||
('super_admin', '超级管理员', '最高权限管理员,可以管理角色和系统配置');
|
('super_admin', '超级管理员', 1, '最高权限管理员,可以管理角色和系统配置');
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- auth_user_roles: 用户角色关联表
|
-- 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
|
```sql
|
||||||
-- 预留:权限点表(后期扩展)
|
-- 预留:权限点表(后期扩展)
|
||||||
@@ -714,8 +718,9 @@ POST /api/v1/admin/auth/login
|
|||||||
GET /api/v1/admin/users
|
GET /api/v1/admin/users
|
||||||
GET /api/v1/admin/users/:id
|
GET /api/v1/admin/users/:id
|
||||||
PUT /api/v1/admin/users/:id/status
|
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
|
POST /api/v1/admin/users
|
||||||
|
GET /api/v1/admin/roles
|
||||||
GET /api/v1/admin/users/:id/meetings
|
GET /api/v1/admin/users/:id/meetings
|
||||||
|
|
||||||
# 会议管理
|
# 会议管理
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
> **状态:✅ Phase 1 全部完成**(2026-03-02)
|
> **状态:✅ Phase 1 全部完成**(2026-03-02)
|
||||||
> 11 个 Task 全部通过端到端验证和代码审查。
|
> 11 个 Task 全部通过端到端验证和代码审查。
|
||||||
> 分支:`feature/phase1-foundation-and-auth`
|
> 分支:`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.
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# EchoChat 项目开发进度
|
# EchoChat 项目开发进度
|
||||||
|
|
||||||
> **最后更新**:2026-03-02(前后端错误处理规范统一 + 安全加固)
|
> **最后更新**:2026-03-02(角色等级体系与权限管控实施)
|
||||||
> **当前阶段**:Phase 1 - 基础设施与用户认证
|
> **当前阶段**:Phase 1 - 基础设施与用户认证
|
||||||
> **当前分支**:`feature/phase1-foundation-and-auth`
|
> **当前分支**:`feature/phase1-foundation-and-auth`
|
||||||
> **实施计划**:`docs/plans/2026-02-27-phase1-foundation-and-auth.md`
|
> **实施计划**:`docs/plans/2026-02-27-phase1-foundation-and-auth.md`
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
5. **API 响应格式**:统一 `{ "code": 0, "message": "success", "data": ... }`
|
5. **API 响应格式**:统一 `{ "code": 0, "message": "success", "data": ... }`
|
||||||
6. **常量命名**:Go camelCase(`UserStatusActive`),非大写下划线
|
6. **常量命名**:Go camelCase(`UserStatusActive`),非大写下划线
|
||||||
7. **模块路由**:模块内自注册 + 中央 router 聚合
|
7. **模块路由**:模块内自注册 + 中央 router 聚合
|
||||||
|
8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行"高等级管理低等级"规则
|
||||||
|
|
||||||
### 前台用户端(frontend/)
|
### 前台用户端(frontend/)
|
||||||
1. **框架**:uni-app 3.0(Vue 3.4.21 框架锁定)
|
1. **框架**:uni-app 3.0(Vue 3.4.21 框架锁定)
|
||||||
@@ -151,7 +152,7 @@ cd frontend && npm run dev:h5
|
|||||||
|
|
||||||
### 可测试功能
|
### 可测试功能
|
||||||
|
|
||||||
- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用 → 分配角色 → 创建用户
|
- **管理端**:登录 → 仪表盘 → 用户列表 → 搜索/筛选 → 用户详情 → 禁用/启用(受角色等级约束) → 批量设置角色(Checkbox 多选 + 等级管控) → 创建用户
|
||||||
- **前台 H5**:注册 → 登录 → 个人中心 → 修改资料 → 退出登录
|
- **前台 H5**:注册 → 登录 → 个人中心 → 修改资料 → 退出登录
|
||||||
- **API**:`GET http://localhost:8085/health`(健康检查)
|
- **API**:`GET http://localhost:8085/health`(健康检查)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user