2026-08-09 13:50:55 +08:00
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-09 16:02:25 +08:00
|
|
|
// AdminRequired 叠加在已认证路由上。采购员能使用业务页,但不能执行管理操作。
|
2026-08-09 13:50:55 +08:00
|
|
|
func AdminRequired() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
user := currentUser(c)
|
|
|
|
|
if user != nil && user.IsAdmin() {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-09 16:02:25 +08:00
|
|
|
fail(c, http.StatusForbidden, "只有管理员可以执行此管理操作。当前账号仍可使用其他业务页面。")
|
2026-08-09 13:50:55 +08:00
|
|
|
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)
|
|
|
|
|
}
|