新增用户管理和个人信息功能,支持CRUD操作和密码修改

This commit is contained in:
2026-07-03 10:18:08 +08:00
parent ca9b1e751c
commit e65b31abb0
8 changed files with 801 additions and 4 deletions
+20 -2
View File
@@ -36,13 +36,31 @@ const navLinks = [
</RouterLink>
<template v-if="authStore.isLoggedIn">
<RouterLink
to="/admin"
to="/profile"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors
hover:bg-sky-500/40"
active-class="bg-sky-700/60"
>
管理后台
用户信息
</RouterLink>
<template v-if="authStore.user?.role === 'admin'">
<RouterLink
to="/admin"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors
hover:bg-sky-500/40"
active-class="bg-sky-700/60"
>
管理后台
</RouterLink>
<RouterLink
to="/admin/users"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors
hover:bg-sky-500/40"
active-class="bg-sky-700/60"
>
用户管理
</RouterLink>
</template>
<a
href="#"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors
+16 -2
View File
@@ -20,11 +20,23 @@ const router = createRouter({
name: 'login',
component: () => import('../views/LoginView.vue'),
},
{
path: '/profile',
name: 'profile',
component: () => import('../views/ProfileView.vue'),
meta: { requiresAuth: true },
},
{
path: '/admin',
name: 'admin',
component: () => import('../views/AdminView.vue'),
meta: { requiresAuth: true },
meta: { requiresAuth: true, adminOnly: true },
},
{
path: '/admin/users',
name: 'users',
component: () => import('../views/UserManageView.vue'),
meta: { requiresAuth: true, adminOnly: true },
},
],
})
@@ -34,8 +46,10 @@ router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
next({ name: 'login' })
} else if (to.meta.adminOnly && authStore.user?.role !== 'admin') {
next({ name: 'profile' })
} else if (to.name === 'login' && authStore.isLoggedIn) {
next({ name: 'admin' })
next({ name: 'profile' })
} else {
next()
}
+125
View File
@@ -0,0 +1,125 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
onMounted(async () => {
await authStore.fetchUser()
})
const showPasswordModal = ref(false)
const passwordForm = ref({ old_password: '', new_password: '', confirm_password: '' })
const passwordError = ref('')
const changingPassword = ref(false)
function openPasswordModal() {
passwordForm.value = { old_password: '', new_password: '', confirm_password: '' }
passwordError.value = ''
showPasswordModal.value = true
}
async function handleChangePassword() {
passwordError.value = ''
if (!passwordForm.value.old_password || !passwordForm.value.new_password) {
passwordError.value = '请填写原密码和新密码'
return
}
if (passwordForm.value.new_password !== passwordForm.value.confirm_password) {
passwordError.value = '两次输入的新密码不一致'
return
}
changingPassword.value = true
try {
const res = await fetch('/api/me/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.token}`,
},
body: JSON.stringify({
old_password: passwordForm.value.old_password,
new_password: passwordForm.value.new_password,
}),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || '密码修改失败')
}
showPasswordModal.value = false
} catch (e: any) {
passwordError.value = e.message || '密码修改失败'
} finally {
changingPassword.value = false
}
}
</script>
<template>
<div class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">用户信息</h2>
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">基本信息</h3>
<div class="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<p><span class="font-medium text-gray-900 dark:text-white">用户名</span>{{ authStore.user?.username }}</p>
<p><span class="font-medium text-gray-900 dark:text-white">角色</span>{{ authStore.user?.role === 'admin' ? '管理员' : '普通用户' }}</p>
</div>
</div>
<button
class="px-6 py-2.5 rounded-lg font-medium text-white bg-sky-600 hover:bg-sky-700 transition-colors"
@click="openPasswordModal"
>
修改密码
</button>
<div v-if="showPasswordModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="showPasswordModal = false">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">修改密码</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">原密码</label>
<input
v-model="passwordForm.old_password"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">新密码</label>
<input
v-model="passwordForm.new_password"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">确认新密码</label>
<input
v-model="passwordForm.confirm_password"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
<p v-if="passwordError" class="text-sm text-red-500">{{ passwordError }}</p>
</div>
<div class="flex justify-end gap-3 mt-6">
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
@click="showPasswordModal = false"
>
取消
</button>
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-white bg-sky-600 hover:bg-sky-700 disabled:opacity-50 transition-colors"
:disabled="changingPassword"
@click="handleChangePassword"
>
{{ changingPassword ? '保存中...' : '确定' }}
</button>
</div>
</div>
</div>
</div>
</template>
+372
View File
@@ -0,0 +1,372 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const users = ref<any[]>([])
const loading = ref(false)
const error = ref('')
interface User {
id: number
username: string
role: string
status: number
created_at: string
updated_at: string
}
async function fetchUsers() {
loading.value = true
error.value = ''
try {
const res = await fetch('/api/admin/users', {
headers: { Authorization: `Bearer ${authStore.token}` },
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || '加载失败')
}
const data = await res.json()
users.value = data.users
} catch (e: any) {
error.value = e.message || '加载失败'
} finally {
loading.value = false
}
}
const showCreateModal = ref(false)
const createForm = ref({ username: '', password: '', role: 'user', status: 1 })
const createError = ref('')
const creating = ref(false)
async function handleCreate() {
createError.value = ''
if (!createForm.value.username || !createForm.value.password) {
createError.value = '请填写用户名和密码'
return
}
creating.value = true
try {
const res = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.token}`,
},
body: JSON.stringify({
username: createForm.value.username,
password: createForm.value.password,
role: createForm.value.role,
status: createForm.value.status,
}),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || '创建失败')
}
showCreateModal.value = false
createForm.value = { username: '', password: '', role: 'user', status: 1 }
await fetchUsers()
} catch (e: any) {
createError.value = e.message || '创建失败'
} finally {
creating.value = false
}
}
const showEditModal = ref(false)
const editingUser = ref<User | null>(null)
const editForm = ref({ role: 'user', status: 1, password: '' })
const editError = ref('')
const saving = ref(false)
function openEditModal(user: User) {
editingUser.value = user
editForm.value = { role: user.role, status: user.status, password: '' }
editError.value = ''
showEditModal.value = true
}
async function handleUpdate() {
if (!editingUser.value) return
editError.value = ''
saving.value = true
try {
const body: any = { role: editForm.value.role, status: editForm.value.status }
if (editForm.value.password) {
body.password = editForm.value.password
}
const res = await fetch(`/api/admin/users/${editingUser.value.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.token}`,
},
body: JSON.stringify(body),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || '保存失败')
}
showEditModal.value = false
await fetchUsers()
} catch (e: any) {
editError.value = e.message || '保存失败'
} finally {
saving.value = false
}
}
const showDeleteConfirm = ref(false)
const deletingUser = ref<User | null>(null)
const deleteError = ref('')
const deleting = ref(false)
function confirmDelete(user: User) {
deletingUser.value = user
deleteError.value = ''
showDeleteConfirm.value = true
}
async function handleDelete() {
if (!deletingUser.value) return
deleteError.value = ''
deleting.value = true
try {
const res = await fetch(`/api/admin/users/${deletingUser.value.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${authStore.token}` },
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error || '删除失败')
}
showDeleteConfirm.value = false
await fetchUsers()
} catch (e: any) {
deleteError.value = e.message || '删除失败'
} finally {
deleting.value = false
}
}
onMounted(() => {
fetchUsers()
})
</script>
<template>
<div class="max-w-6xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">用户管理</h2>
<button
class="px-4 py-2 rounded-lg font-medium text-white bg-sky-600 hover:bg-sky-700 transition-colors"
@click="showCreateModal = true"
>
新增用户
</button>
</div>
<p v-if="error" class="text-red-500 mb-4">{{ error }}</p>
<div v-if="loading" class="text-gray-500 dark:text-gray-400 py-8 text-center">加载中...</div>
<div v-else class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">ID</th>
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">用户名</th>
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">角色</th>
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">状态</th>
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">创建时间</th>
<th class="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="user in users"
:key="user.id"
class="border-b border-gray-100 dark:border-gray-700/50 hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors"
>
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
<td class="px-4 py-3 text-gray-900 dark:text-white font-medium">{{ user.username }}</td>
<td class="px-4 py-3">
<span
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
:class="user.role === 'admin' ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'"
>
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
</span>
</td>
<td class="px-4 py-3">
<span
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium"
:class="user.status === 1 ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'"
>
{{ user.status === 1 ? '启用' : '禁用' }}
</span>
</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ user.created_at }}</td>
<td class="px-4 py-3">
<div class="flex gap-2">
<button
class="px-3 py-1 text-xs rounded-md font-medium text-sky-700 bg-sky-50 hover:bg-sky-100 dark:text-sky-400 dark:bg-sky-900/20 dark:hover:bg-sky-900/40 transition-colors"
@click="openEditModal(user)"
>
编辑
</button>
<button
class="px-3 py-1 text-xs rounded-md font-medium text-red-700 bg-red-50 hover:bg-red-100 dark:text-red-400 dark:bg-red-900/20 dark:hover:bg-red-900/40 transition-colors"
@click="confirmDelete(user)"
>
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="showCreateModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="showCreateModal = false">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">新增用户</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">用户名</label>
<input
v-model="createForm.username"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 placeholder-gray-400 dark:placeholder-gray-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">密码</label>
<input
v-model="createForm.password"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 placeholder-gray-400 dark:placeholder-gray-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">角色</label>
<select
v-model="createForm.role"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">状态</label>
<select
v-model.number="createForm.status"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
>
<option :value="1">启用</option>
<option :value="0">禁用</option>
</select>
</div>
<p v-if="createError" class="text-sm text-red-500">{{ createError }}</p>
</div>
<div class="flex justify-end gap-3 mt-6">
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
@click="showCreateModal = false"
>
取消
</button>
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-white bg-sky-600 hover:bg-sky-700 disabled:opacity-50 transition-colors"
:disabled="creating"
@click="handleCreate"
>
{{ creating ? '创建中...' : '确定' }}
</button>
</div>
</div>
</div>
<div v-if="showEditModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="showEditModal = false">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">编辑用户 - {{ editingUser?.username }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">角色</label>
<select
v-model="editForm.role"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">状态</label>
<select
v-model.number="editForm.status"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500"
>
<option :value="1">启用</option>
<option :value="0">禁用</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">新密码留空不修改</label>
<input
v-model="editForm.password"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 placeholder-gray-400 dark:placeholder-gray-500"
placeholder="留空则不修改密码"
/>
</div>
<p v-if="editError" class="text-sm text-red-500">{{ editError }}</p>
</div>
<div class="flex justify-end gap-3 mt-6">
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
@click="showEditModal = false"
>
取消
</button>
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-white bg-sky-600 hover:bg-sky-700 disabled:opacity-50 transition-colors"
:disabled="saving"
@click="handleUpdate"
>
{{ saving ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
<div v-if="showDeleteConfirm" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="showDeleteConfirm = false">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">确认删除</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
确定要删除用户 <span class="font-medium text-gray-900 dark:text-white">{{ deletingUser?.username }}</span> 此操作不可撤销
</p>
<p v-if="deleteError" class="text-sm text-red-500 mb-4">{{ deleteError }}</p>
<div class="flex justify-end gap-3">
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
@click="showDeleteConfirm = false"
>
取消
</button>
<button
class="px-4 py-2 rounded-lg text-sm font-medium text-white bg-red-500 hover:bg-red-600 disabled:opacity-50 transition-colors"
:disabled="deleting"
@click="handleDelete"
>
{{ deleting ? '删除中...' : '确认删除' }}
</button>
</div>
</div>
</div>
</div>
</template>
+44
View File
@@ -66,6 +66,50 @@ func Login(c *gin.Context) {
})
}
type changePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
func ChangePassword(c *gin.Context) {
var req changePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入原密码和新密码"})
return
}
if len(req.NewPassword) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码长度不能少于6位"})
return
}
userID, _ := c.Get("user_id")
var user model.User
if err := db.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "原密码错误"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
if err := db.DB.Model(&user).Update("password", string(hash)).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码修改失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
}
func Me(c *gin.Context) {
userID, _ := c.Get("user_id")
username, _ := c.Get("username")
+202
View File
@@ -0,0 +1,202 @@
package handler
import (
"net/http"
"strconv"
"lmvpn/internal/db"
"lmvpn/internal/model"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type createUserRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Role string `json:"role"`
Status *int `json:"status"`
}
type updateUserRequest struct {
Status *int `json:"status"`
Role string `json:"role"`
Password string `json:"password"`
}
type userResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
Status int `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func formatUser(u *model.User) userResponse {
return userResponse{
ID: u.ID,
Username: u.Username,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: u.UpdatedAt.Format("2006-01-02 15:04:05"),
}
}
func ListUsers(c *gin.Context) {
var users []model.User
db.DB.Order("id asc").Find(&users)
result := make([]userResponse, len(users))
for i, u := range users {
result[i] = formatUser(&u)
}
c.JSON(http.StatusOK, gin.H{"users": result})
}
func CreateUser(c *gin.Context) {
var req createUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
var exist model.User
if err := db.DB.Where("username = ?", req.Username).First(&exist).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
user := model.User{
Username: req.Username,
Password: string(hash),
Role: "user",
Status: 1,
}
if req.Role != "" {
user.Role = req.Role
}
if req.Status != nil {
user.Status = *req.Status
}
if err := db.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
return
}
c.JSON(http.StatusOK, formatUser(&user))
}
func UpdateUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
currentUserID, _ := c.Get("user_id")
var user model.User
if err := db.DB.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
var req updateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
updates := map[string]interface{}{}
if req.Status != nil {
updates["status"] = *req.Status
}
if req.Role != "" {
if user.ID == currentUserID.(uint) {
c.JSON(http.StatusBadRequest, gin.H{"error": "不能修改自己的角色"})
return
}
if req.Role != "admin" && user.Role == "admin" {
var adminCount int64
db.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&adminCount)
if adminCount <= 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "系统至少需要保留一个管理员"})
return
}
}
updates["role"] = req.Role
}
if req.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
updates["password"] = string(hash)
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有需要更新的字段"})
return
}
if err := db.DB.Model(&user).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户失败"})
return
}
db.DB.First(&user, id)
c.JSON(http.StatusOK, formatUser(&user))
}
func DeleteUser(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
currentUserID, _ := c.Get("user_id")
if uint(id) == currentUserID.(uint) {
c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除自己"})
return
}
var user model.User
if err := db.DB.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if user.Role == "admin" {
var adminCount int64
db.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&adminCount)
if adminCount <= 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "系统至少需要保留一个管理员"})
return
}
}
if err := db.DB.Delete(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
+12
View File
@@ -51,6 +51,18 @@ func ParseToken(tokenStr string) (*Claims, error) {
return nil, jwt.ErrSignatureInvalid
}
func AdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := c.Get("role")
if role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "无权限访问"})
c.Abort()
return
}
c.Next()
}
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
+10
View File
@@ -20,6 +20,16 @@ func Setup(r *gin.Engine) {
auth.Use(middleware.AuthMiddleware())
{
auth.GET("/me", handler.Me)
auth.PUT("/me/password", handler.ChangePassword)
}
admin := r.Group("/api/admin")
admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware())
{
admin.GET("/users", handler.ListUsers)
admin.POST("/users", handler.CreateUser)
admin.PUT("/users/:id", handler.UpdateUser)
admin.DELETE("/users/:id", handler.DeleteUser)
}
fs := http.FileServer(http.Dir("./dist"))