package handlers import ( "errors" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "go_blog/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) // userForm 是后台用户创建/更新接口的 JSON 请求体, // 同时供共享创建/编辑表单模板的渲染使用(IsEdit/Action/TitleText 非绑定字段)。 type userForm struct { ID uint `json:"-"` Username string `json:"username"` Password string `json:"password"` DisplayName string `json:"display_name"` Email string `json:"email"` Gender string `json:"gender"` Birthday string `json:"birthday"` // 来自日期输入的 YYYY-MM-DD Role string `json:"role"` Status int `json:"status"` IsEdit bool Action string TitleText string } // userListView 为 User 附加预渲染的标签/徽章, // 使模板绝不调用包装为 interface{} 的结构体上的方法。 type userListView struct { models.User RoleLabel string RoleBadge string StatusLabel string StatusBadge string } // parseUserFormJSON 绑定 JSON 请求体的用户字段并去除空白。 // 绑定失败时已写入 400 响应并返回 ok=false。ID 来自路由参数。 func parseUserFormJSON(c *gin.Context) (userForm, bool) { var f userForm if !bindJSON(c, &f) { return f, false } f.ID = uintFormID(c.Param("id")) f.Username = strings.TrimSpace(f.Username) f.DisplayName = strings.TrimSpace(f.DisplayName) f.Email = strings.TrimSpace(f.Email) f.Gender = strings.TrimSpace(f.Gender) f.Birthday = strings.TrimSpace(f.Birthday) f.Role = strings.TrimSpace(f.Role) return f, true } // 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) } // errLastAdminRemoval 与 lastAdminMu 实现"最后管理员"防线 // (SECURITY_TODO #30)。原实现先计数再写入,两者之间无原子性: // 两个并发的"降级/删除倒数第二位管理员"请求可同时通过检查, // 导致站点失去管理员。修复后检查与写入包进同一事务: // - MySQL:事务内 SELECT ... FOR UPDATE 锁定管理员集合, // 并发事务在锁上排队,先提交者生效,后到者重读计数后拒绝; // - SQLite:不支持 FOR UPDATE,但其写锁串行化 + 本进程互斥锁 // (应用按设计单实例部署,见 LoginRateLimiter 注释)双保险。 var errLastAdminRemoval = errors.New("cannot remove the last admin") var lastAdminMu sync.Mutex // ensureNotLastAdmin 在事务内读取管理员数量(MySQL 下为锁定读取)。 // 仅剩 1 位(或 0 位)管理员时返回 errLastAdminRemoval。 func ensureNotLastAdmin(tx *gorm.DB) error { q := tx.Model(&models.User{}).Where("role = ?", models.RoleAdmin) if tx.Dialector.Name() != "sqlite" { q = q.Clauses(clause.Locking{Strength: "UPDATE"}) } var admins []models.User if err := q.Find(&admins).Error; err != nil { return err } if len(admins) <= 1 { return errLastAdminRemoval } return nil } // 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 } //