用户管理: 编辑用户信息(用户名/邮箱/重置密码/角色/状态)
- AdminPatchUser 扩展 username/email/password 更新, 含唯一性与格式校验 - 编辑弹窗补充用户名/邮箱/重置密码字段(留空不改密码) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -46,6 +48,9 @@ func (h *Handler) AdminPatchUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var req struct {
|
var req struct {
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Email *string `json:"email"`
|
||||||
|
Password *string `json:"password"`
|
||||||
Role *string `json:"role"`
|
Role *string `json:"role"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
}
|
}
|
||||||
@@ -54,6 +59,46 @@ func (h *Handler) AdminPatchUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
updates := map[string]any{}
|
updates := map[string]any{}
|
||||||
|
if req.Username != nil {
|
||||||
|
u := strings.TrimSpace(*req.Username)
|
||||||
|
if len(u) < 3 || len(u) > 32 {
|
||||||
|
resp.Fail(c, http.StatusBadRequest, "username must be 3-32 chars")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var n int64
|
||||||
|
h.a.DB.Model(&store.User{}).Where("username = ? AND id != ?", u, id).Count(&n)
|
||||||
|
if n > 0 {
|
||||||
|
resp.Fail(c, http.StatusConflict, "username already taken")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates["username"] = u
|
||||||
|
}
|
||||||
|
if req.Email != nil {
|
||||||
|
e := strings.ToLower(strings.TrimSpace(*req.Email))
|
||||||
|
if _, err := mail.ParseAddress(e); err != nil {
|
||||||
|
resp.Fail(c, http.StatusBadRequest, "invalid email")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var n int64
|
||||||
|
h.a.DB.Model(&store.User{}).Where("email = ? AND id != ?", e, id).Count(&n)
|
||||||
|
if n > 0 {
|
||||||
|
resp.Fail(c, http.StatusConflict, "email already taken")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates["email"] = e
|
||||||
|
}
|
||||||
|
if req.Password != nil && *req.Password != "" {
|
||||||
|
if len(*req.Password) < 8 {
|
||||||
|
resp.Fail(c, http.StatusBadRequest, "password must be at least 8 chars")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := h.a.Hasher.HashPassword(*req.Password)
|
||||||
|
if err != nil {
|
||||||
|
resp.Fail(c, http.StatusInternalServerError, "failed to hash password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates["password_hash"] = hash
|
||||||
|
}
|
||||||
if req.Role != nil {
|
if req.Role != nil {
|
||||||
if *req.Role != store.RoleUser && *req.Role != store.RoleAdmin {
|
if *req.Role != store.RoleUser && *req.Role != store.RoleAdmin {
|
||||||
resp.Fail(c, http.StatusBadRequest, "role must be user or admin")
|
resp.Fail(c, http.StatusBadRequest, "role must be user or admin")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const pageSize = 15
|
|||||||
|
|
||||||
const editOpen = ref(false)
|
const editOpen = ref(false)
|
||||||
const editing = ref<User | null>(null)
|
const editing = ref<User | null>(null)
|
||||||
const editForm = reactive({ role: 'user', status: 'active' })
|
const editForm = reactive({ username: '', email: '', password: '', role: 'user', status: 'active' })
|
||||||
|
|
||||||
const balanceOpen = ref(false)
|
const balanceOpen = ref(false)
|
||||||
const balanceUser = ref<User | null>(null)
|
const balanceUser = ref<User | null>(null)
|
||||||
@@ -43,7 +43,13 @@ function search() {
|
|||||||
|
|
||||||
function openEdit(u: User) {
|
function openEdit(u: User) {
|
||||||
editing.value = u
|
editing.value = u
|
||||||
Object.assign(editForm, { role: u.role, status: u.status })
|
Object.assign(editForm, {
|
||||||
|
username: u.username,
|
||||||
|
email: u.email,
|
||||||
|
password: '',
|
||||||
|
role: u.role,
|
||||||
|
status: u.status,
|
||||||
|
})
|
||||||
editOpen.value = true
|
editOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,23 +166,34 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑用户 -->
|
<!-- 编辑用户 -->
|
||||||
<Modal :open="editOpen" title="编辑用户" @close="editOpen = false">
|
<Modal :open="editOpen" :title="`编辑用户 · ${editing?.username}`" @close="editOpen = false">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<Input v-model="editForm.username" label="用户名" />
|
||||||
|
<Input v-model="editForm.email" label="邮箱" type="email" />
|
||||||
|
<Input
|
||||||
|
v-model="editForm.password"
|
||||||
|
label="重置密码"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
hint="留空则不修改"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<span class="mb-1.5 block text-xs font-medium text-muted">角色</span>
|
<span class="mb-1.5 block text-xs font-medium text-muted">角色</span>
|
||||||
<select v-model="editForm.role" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm outline-none focus:border-accent">
|
<select v-model="editForm.role" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent">
|
||||||
<option value="user">user</option>
|
<option value="user">user</option>
|
||||||
<option value="admin">admin</option>
|
<option value="admin">admin</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<span class="mb-1.5 block text-xs font-medium text-muted">状态</span>
|
<span class="mb-1.5 block text-xs font-medium text-muted">状态</span>
|
||||||
<select v-model="editForm.status" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm outline-none focus:border-accent">
|
<select v-model="editForm.status" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent">
|
||||||
<option value="active">active</option>
|
<option value="active">active</option>
|
||||||
<option value="disabled">disabled</option>
|
<option value="disabled">disabled</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button variant="ghost" @click="editOpen = false">取消</Button>
|
<Button variant="ghost" @click="editOpen = false">取消</Button>
|
||||||
<Button @click="saveEdit">保存</Button>
|
<Button @click="saveEdit">保存</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user