Files
go_blog/handlers/admin_user.go
T
kevin f307781f58 docs: 全部 Go 代码注释汉化
- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文
- 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等
- 代码、字符串字面量、日志消息保持英文原文,零逻辑改动
- go build/vet 通过,go test -count=1 ./... 全绿
2026-08-27 19:03:03 +08:00

420 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
// 状态始终由 <select>0/1/2/3)发送;缺省视为正常,
// 但显式的 0(禁用)必须保留。
status := models.StatusNormal
if raw := strings.TrimSpace(c.PostForm("status")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil {
status = n
}
}
return userForm{
ID: uintFormID(c.Param("id")),
Username: strings.TrimSpace(c.PostForm("username")),
Password: c.PostForm("password"),
DisplayName: strings.TrimSpace(c.PostForm("display_name")),
Email: strings.TrimSpace(c.PostForm("email")),
Gender: strings.TrimSpace(c.PostForm("gender")),
Birthday: strings.TrimSpace(c.PostForm("birthday")),
Role: strings.TrimSpace(c.PostForm("role")),
Status: status,
}
}
// uintFormID 将路由 :id 解析为 uint(不存在/非法时为 0)。
func uintFormID(s string) uint {
if s == "" {
return 0
}
n, err := strconv.ParseUint(s, 10, 64)
if err != nil {
return 0
}
return uint(n)
}
// applyUserFormToData 将表单值写入模板数据映射,
// 使渲染时表单被重新填充(初次加载或校验错误)。
func applyUserFormToData(data gin.H, f userForm) {
data["FormID"] = f.ID
data["FormUsername"] = f.Username
data["FormPassword"] = f.Password
data["FormDisplayName"] = f.DisplayName
data["FormEmail"] = f.Email
data["FormGender"] = f.Gender
data["FormBirthday"] = f.Birthday
data["FormRole"] = f.Role
data["FormStatus"] = f.Status
data["FormIsEdit"] = f.IsEdit
data["FormAction"] = f.Action
data["FormTitleText"] = f.TitleText
}
// renderUserForm 使用给定的表单值和可选的错误消息渲染共享的用户表单模板。
func renderUserForm(c *gin.Context, f userForm, errMsg string) {
tr := getTr(c)
data := DefaultData(c)
data["Title"] = f.TitleText
if errMsg != "" {
data["Error"] = errMsg
}
// <select> 元素的角色/状态选项。
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")
}
}