feat: 实现 Admin 采购员账号管理 (#51)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/repository"
|
||||
"cmautobuy/admin/service"
|
||||
)
|
||||
|
||||
// AdminRequired 叠加在已认证的 /users 路由上。采购员能使用业务页,但不能管理账号。
|
||||
func AdminRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
user := currentUser(c)
|
||||
if user != nil && user.IsAdmin() {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
fail(c, http.StatusForbidden, "只有管理员可以访问用户管理。当前账号仍可使用其他业务页面。")
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
// UserList 显示用户搜索、分页和管理操作。
|
||||
func (h *Handler) UserList(c *gin.Context) {
|
||||
h.renderUserList(c, http.StatusOK, c.Query("msg"), c.Query("error"), false, "")
|
||||
}
|
||||
|
||||
func (h *Handler) renderUserList(c *gin.Context, httpStatus int, message, errorMessage string, needCreate bool, createUsername string) {
|
||||
keyword := strings.TrimSpace(c.Query("q"))
|
||||
result, err := service.ListUsers(h.db, currentUser(c), keyword,
|
||||
model.UserStatus(c.Query("status")), service.ParsePage(c.Query("page")))
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "读取用户列表失败,账号数据没有被改动。刷新后重试。")
|
||||
return
|
||||
}
|
||||
values := url.Values{}
|
||||
if keyword != "" {
|
||||
values.Set("q", keyword)
|
||||
}
|
||||
if result.Status != "" {
|
||||
values.Set("status", string(result.Status))
|
||||
}
|
||||
status := fmt.Sprintf("共 %d 个用户 · 第 %d/%d 页", result.Total, result.Page, result.TotalPages)
|
||||
c.HTML(httpStatus, "user/list", page(c, "users", "用户管理", gin.H{
|
||||
"Rows": result.Rows, "Keyword": result.Keyword, "Status": status,
|
||||
"Pagination": service.NewPaginationView(result.Page, result.TotalPages, values.Encode()),
|
||||
"StatusFilter": result.Status, "Message": message, "Error": errorMessage,
|
||||
"NeedCreate": needCreate, "CreateUsername": createUsername,
|
||||
}))
|
||||
}
|
||||
|
||||
// UserCreate 只创建固定 purchaser 角色,密码不写入 URL、不回显。
|
||||
func (h *Handler) UserCreate(c *gin.Context) {
|
||||
err := service.CreatePurchaser(h.db, currentUser(c), c.PostForm("username"),
|
||||
c.PostForm("password"), c.PostForm("password_confirm"), time.Now())
|
||||
if err != nil {
|
||||
if isExpectedUserError(err) {
|
||||
h.renderUserList(c, http.StatusBadRequest, "", err.Error(), true,
|
||||
strings.TrimSpace(c.PostForm("username")))
|
||||
return
|
||||
}
|
||||
fail(c, http.StatusInternalServerError, "创建采购员失败,已有账号数据保持不变。刷新后重试。")
|
||||
return
|
||||
}
|
||||
redirectUsers(c, "采购员创建成功", "")
|
||||
}
|
||||
|
||||
// UserSetStatus 启用或禁用账号,并由 Repository 在同一事务撤销全部 Session。
|
||||
func (h *Handler) UserSetStatus(c *gin.Context) {
|
||||
status := model.UserStatus(c.PostForm("status"))
|
||||
err := service.ChangeUserStatus(h.db, currentUser(c), c.PostForm("user_id"), status, time.Now())
|
||||
if err != nil {
|
||||
h.userActionFailure(c, err, "修改账号状态失败")
|
||||
return
|
||||
}
|
||||
message := "账号已启用"
|
||||
if status == model.UserDisabled {
|
||||
message = "账号已禁用,原有登录已撤销"
|
||||
}
|
||||
redirectUsers(c, message, "")
|
||||
}
|
||||
|
||||
// UserResetPassword 重置密码并撤销目标用户全部 Session。密码不会回显。
|
||||
func (h *Handler) UserResetPassword(c *gin.Context) {
|
||||
err := service.ResetUserPassword(h.db, currentUser(c), c.PostForm("user_id"),
|
||||
c.PostForm("password"), c.PostForm("password_confirm"), time.Now())
|
||||
if err != nil {
|
||||
h.userActionFailure(c, err, "重置密码失败")
|
||||
return
|
||||
}
|
||||
redirectUsers(c, "密码已重置,原有登录已撤销", "")
|
||||
}
|
||||
|
||||
func (h *Handler) userActionFailure(c *gin.Context, err error, fallback string) {
|
||||
if isExpectedUserError(err) {
|
||||
redirectUsers(c, "", err.Error())
|
||||
return
|
||||
}
|
||||
fail(c, http.StatusInternalServerError, fallback+",已有账号数据保持不变。刷新后重试。")
|
||||
}
|
||||
|
||||
func isExpectedUserError(err error) bool {
|
||||
return service.IsValidationError(err) || errors.Is(err, repository.ErrUsernameExists) ||
|
||||
errors.Is(err, repository.ErrUserNotFound) || errors.Is(err, repository.ErrLastActiveAdmin)
|
||||
}
|
||||
|
||||
func redirectUsers(c *gin.Context, message, errorMessage string) {
|
||||
query := url.Values{}
|
||||
if message != "" {
|
||||
query.Set("msg", message)
|
||||
}
|
||||
if errorMessage != "" {
|
||||
query.Set("error", errorMessage)
|
||||
}
|
||||
location := "/users"
|
||||
if encoded := query.Encode(); encoded != "" {
|
||||
location += "?" + encoded
|
||||
}
|
||||
c.Redirect(http.StatusSeeOther, location)
|
||||
}
|
||||
@@ -83,6 +83,13 @@ func Register(r *gin.Engine, db *sql.DB, onlineThreshold time.Duration) {
|
||||
// 5. 客户端列表
|
||||
pages.GET("/clients", h.ClientList)
|
||||
pages.POST("/clients/delete", h.ClientDelete)
|
||||
|
||||
// 6. 用户管理:先经过网页登录,再叠加管理员角色校验。
|
||||
users := pages.Group("/users", AdminRequired())
|
||||
users.GET("", h.UserList)
|
||||
users.POST("/create", h.UserCreate)
|
||||
users.POST("/status", h.UserSetStatus)
|
||||
users.POST("/reset-password", h.UserResetPassword)
|
||||
}
|
||||
|
||||
// page 组装每个页面都要的公共数据(导航高亮、标题、CSRF token)。
|
||||
|
||||
Reference in New Issue
Block a user