package handlers import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "go_blog/models" "gorm.io/gorm" ) // userForm 保存提交的用户字段,外加共享创建/编辑表单模板的渲染元数据。 type userForm struct { ID uint Username string Password string DisplayName string Email string Gender string Birthday string // 来自日期输入的 YYYY-MM-DD Role string Status int IsEdit bool Action string TitleText string } // userListView 为 User 附加预渲染的标签/徽章, // 使模板绝不调用包装为 interface{} 的结构体上的方法。 type userListView struct { models.User RoleLabel string RoleBadge string StatusLabel string StatusBadge string } // parseUserForm 从请求中读取用户表单字段。 func parseUserForm(c *gin.Context) userForm { // 状态始终由 元素的角色/状态选项。 data["RoleAdmin"] = models.RoleAdmin data["RoleAuthor"] = models.RoleAuthor data["StatusNormal"] = models.StatusNormal data["StatusDisabled"] = models.StatusDisabled data["StatusLocked"] = models.StatusLocked data["StatusUnactivated"] = models.StatusUnactivated data["TrGenderMale"] = tr["gender_male"] data["TrGenderFemale"] = tr["gender_female"] data["TrGenderOther"] = tr["gender_other"] applyUserFormToData(data, f) c.HTML(http.StatusOK, "user_form", data) } // UserListPage 渲染后台用户管理列表。 func UserListPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) var users []models.User db.Order("created_at DESC").Find(&users) views := make([]userListView, 0, len(users)) for _, u := range users { v := userListView{User: u} switch u.Role { case models.RoleAdmin: v.RoleLabel = tr["role_admin"] v.RoleBadge = "bg-purple-100 text-purple-700" default: v.RoleLabel = tr["role_author"] v.RoleBadge = "bg-blue-100 text-blue-700" } switch u.Status { case models.StatusNormal: v.StatusLabel = tr["status_normal"] v.StatusBadge = "bg-green-100 text-green-700" case models.StatusDisabled: v.StatusLabel = tr["status_disabled"] v.StatusBadge = "bg-gray-200 text-gray-600" case models.StatusLocked: v.StatusLabel = tr["status_locked"] v.StatusBadge = "bg-yellow-100 text-yellow-700" default: v.StatusLabel = tr["status_unactivated"] v.StatusBadge = "bg-red-100 text-red-700" } views = append(views, v) } data := DefaultData(c) data["Title"] = tr["admin_users_title"] data["Users"] = views if msg := c.Query("saved"); msg == "1" { switch c.Query("msg") { case "created": data["Success"] = tr["user_created"] case "updated": data["Success"] = tr["user_updated"] case "deleted": data["Success"] = tr["user_deleted"] default: data["Success"] = tr["settings_saved"] } } if e := c.Query("error"); e != "" { switch e { case "self_disable": data["Error"] = tr["user_cannot_disable_self"] case "last_admin": data["Error"] = tr["user_cannot_remove_last_admin"] case "exists": data["Error"] = tr["user_username_exists"] } } c.HTML(http.StatusOK, "user_list", data) } } // UserCreatePage 渲染空白用户创建表单。 func UserCreatePage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) renderUserForm(c, userForm{ Action: "/admin/users/new", TitleText: tr["user_create_title"], Role: models.RoleAuthor, Status: models.StatusNormal, IsEdit: false, }, "") } } // UserCreate 处理 POST 创建新用户。 func UserCreate(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) f := parseUserForm(c) f.Action = "/admin/users/new" f.TitleText = tr["user_create_title"] f.IsEdit = false if f.Username == "" { renderUserForm(c, f, tr["user_username_required"]) return } if f.Password == "" { renderUserForm(c, f, tr["user_password_required"]) return } // SECURITY (#23):执行平台最小密码长度。 if !validatePassword(f.Password) { renderUserForm(c, f, tr["user_password_short"]) return } // SECURITY (#24):拒绝格式非法的邮箱地址。 if !validateEmail(f.Email) { renderUserForm(c, f, tr["user_email_invalid"]) return } if f.Role == "" { f.Role = models.RoleAuthor } // 用户名必须唯一。 var exists int64 db.Model(&models.User{}).Where("username = ?", f.Username).Count(&exists) if exists > 0 { renderUserForm(c, f, tr["user_username_exists"]) return } user := models.User{ Username: f.Username, DisplayName: f.DisplayName, Email: f.Email, Gender: f.Gender, Role: f.Role, Status: f.Status, } if t, err := time.Parse("2006-01-02", f.Birthday); err == nil { user.Birthday = &t } if err := user.SetPassword(f.Password); err != nil { renderUserForm(c, f, tr["article_error"]) return } if err := db.Create(&user).Error; err != nil { renderUserForm(c, f, tr["article_error"]) return } c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=created") } } // UserEditPage 渲染预填现有用户的编辑表单。 func UserEditPage(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) // SECURITY (#19):路由参数必须先解析为数值 id 再交给 GORM—— // 原始字符串作为单一条件传给 First() 时会按 SQL WHERE 子句拼接。 id := uintFormID(c.Param("id")) if id == 0 { c.Redirect(http.StatusFound, "/admin/users") return } var user models.User if err := db.First(&user, id).Error; err != nil { c.Redirect(http.StatusFound, "/admin/users") return } birthday := "" if user.Birthday != nil { birthday = user.Birthday.Format("2006-01-02") } renderUserForm(c, userForm{ ID: user.ID, Username: user.Username, DisplayName: user.DisplayName, Email: user.Email, Gender: user.Gender, Birthday: birthday, Role: user.Role, Status: user.Status, IsEdit: true, Action: fmt.Sprintf("/admin/users/%d/edit", user.ID), TitleText: tr["user_edit_title"], }, "") } } // UserUpdate 处理 POST 更新现有用户。 func UserUpdate(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { tr := getTr(c) f := parseUserForm(c) // SECURITY (#19):在触碰 GORM 前拒绝非数值 id(参见 UserEditPage)。 if f.ID == 0 { c.Redirect(http.StatusFound, "/admin/users") return } f.IsEdit = true f.Action = fmt.Sprintf("/admin/users/%d/edit", f.ID) f.TitleText = tr["user_edit_title"] var user models.User if err := db.First(&user, f.ID).Error; err != nil { c.Redirect(http.StatusFound, "/admin/users") return } // 保留原用户名(它是登录键 + 文章外键的来源)。 f.Username = user.Username // SECURITY (#23/#24):在进行任何其他修改前校验提交的密码/邮箱—— // 密码重置或个人资料编辑必须遵守与注册相同的规则。 if f.Password != "" && !validatePassword(f.Password) { renderUserForm(c, f, tr["user_password_short"]) return } if !validateEmail(f.Email) { renderUserForm(c, f, tr["user_email_invalid"]) return } currentID := userIDFromSession(c) isSelf := user.ID == currentID // 自我保护:不能禁用/锁定自己的账户。 if isSelf && f.Status != models.StatusNormal { f.DisplayName = user.DisplayName f.Email = user.Email f.Gender = user.Gender f.Role = user.Role f.Status = user.Status if user.Birthday != nil { f.Birthday = user.Birthday.Format("2006-01-02") } renderUserForm(c, f, tr["user_cannot_disable_self"]) return } // 自我保护:不能降级最后一位管理员。 if user.Role == models.RoleAdmin && f.Role != models.RoleAdmin { var adminCount int64 db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&adminCount) if adminCount <= 1 { c.Redirect(http.StatusFound, "/admin/users?error=last_admin") return } } if f.Role == "" { f.Role = user.Role } user.DisplayName = f.DisplayName user.Email = f.Email user.Gender = f.Gender user.Role = f.Role user.Status = f.Status if t, err := time.Parse("2006-01-02", f.Birthday); err == nil { user.Birthday = &t } else { user.Birthday = nil } // 可选密码重置(留空表示保持当前密码)。 if f.Password != "" { if err := user.SetPassword(f.Password); err != nil { renderUserForm(c, f, tr["article_error"]) return } } if err := db.Save(&user).Error; err != nil { renderUserForm(c, f, tr["article_error"]) return } c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=updated") } } // UserDelete 软删除用户,带自我保护和最后管理员防线。 func UserDelete(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // SECURITY (#19):在触碰 GORM 前拒绝非数值 id(参见 UserEditPage)。 targetID := uintFormID(c.Param("id")) if targetID == 0 { c.Redirect(http.StatusFound, "/admin/users") return } currentID := userIDFromSession(c) var user models.User if err := db.First(&user, targetID).Error; err != nil { c.Redirect(http.StatusFound, "/admin/users") return } // 不能删除自己。 if targetID == currentID { c.Redirect(http.StatusFound, "/admin/users?error=self_disable") return } // 不能删除最后一位管理员。 if user.Role == models.RoleAdmin { var adminCount int64 db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&adminCount) if adminCount <= 1 { c.Redirect(http.StatusFound, "/admin/users?error=last_admin") return } } db.Delete(&user) c.Redirect(http.StatusFound, "/admin/users?saved=1&msg=deleted") } }