Files
2026-05-04 16:20:46 +08:00

412 lines
13 KiB
Go
Raw Permalink 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 controllers
import (
"errors"
"hr_receiver/config"
"hr_receiver/models"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type UserAdminController struct {
DB *gorm.DB
}
type userAdminPayload struct {
Email *string `json:"email"`
FlavorType models.UserFlavorType `json:"flavorType"`
IsActive *bool `json:"isActive"`
Password string `json:"password"`
Phone *string `json:"phone"`
RegionIDs []uint32 `json:"regionIds"`
RevokeTokens *bool `json:"revokeTokens"`
Role models.UserRole `json:"role"`
Username string `json:"username"`
}
type userAdminListItem struct {
CreatedAt int64 `json:"created_at"`
Email *string `json:"email"`
FlavorType models.UserFlavorType `json:"flavorType"`
ID uint `json:"id"`
IsActive bool `json:"isActive"`
KindergartenNames []string `json:"kindergartenNames"`
Phone *string `json:"phone"`
RegionIDs []uint32 `json:"regionIds"`
Regions []models.UserRegionBinding `json:"regions"`
Role models.UserRole `json:"role"`
TokenInvalidBefore int64 `json:"tokenInvalidBefore"`
UpdatedAt int64 `json:"updated_at"`
Username string `json:"username"`
}
func NewUserAdminController() *UserAdminController {
return &UserAdminController{DB: config.DB}
}
// @Summary 获取用户列表
// @Description 查询系统用户列表,支持按关键词/角色/类型筛选
// @Tags 用户管理
// @Produce json
// @Param keyword query string false "关键词(用户名/邮箱/手机号模糊搜索)"
// @Param role query string false "角色筛选"
// @Param flavorType query string false "类型筛选"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/users [get]
func (uc *UserAdminController) List(c *gin.Context) {
var users []models.User
query := uc.DB.Preload("Regions").Order("id ASC")
if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" {
likeValue := "%" + keyword + "%"
query = query.Where("username LIKE ? OR email LIKE ? OR phone LIKE ?", likeValue, likeValue, likeValue)
}
if role := strings.TrimSpace(c.Query("role")); role != "" {
query = query.Where("role = ?", role)
}
if flavorType := strings.TrimSpace(c.Query("flavorType")); flavorType != "" {
query = query.Where("flavor_type = ?", flavorType)
}
if err := query.Find(&users).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query users")
return
}
items, err := uc.buildUserAdminItems(users)
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to load kindergarten names")
return
}
writeSuccess(c, http.StatusOK, "query success", items)
}
// @Summary 创建用户
// @Description 创建新的系统用户
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param user body userAdminPayload true "用户信息"
// @Security BearerAuth
// @Success 201 {object} SwagAPIResponse "创建成功"
// @Failure 400 {object} SwagAPIResponse "请求参数错误"
// @Router /admin/users [post]
func (uc *UserAdminController) Create(c *gin.Context) {
var payload userAdminPayload
if err := c.ShouldBindJSON(&payload); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if err := validateCreateUserPayload(payload); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if err := validateRegionIDs(payload.RegionIDs); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
user := models.User{
Email: normalizeOptionalString(payload.Email),
FlavorType: payload.FlavorType,
Phone: normalizeOptionalString(payload.Phone),
Password: payload.Password,
Role: payload.Role,
Username: strings.TrimSpace(payload.Username),
Regions: buildUserRegionBindings(payload.RegionIDs),
}
if payload.IsActive != nil {
user.IsActive = *payload.IsActive
}
if err := uc.DB.Create(&user).Error; err != nil {
writeUserAdminDBError(c, err)
return
}
if payload.IsActive != nil && !*payload.IsActive {
if err := uc.DB.Model(&models.User{}).Where("id = ?", user.ID).Update("is_active", false).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to set user status")
return
}
}
if err := uc.DB.Preload("Regions").First(&user, user.ID).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to reload user")
return
}
items, err := uc.buildUserAdminItems([]models.User{user})
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to load kindergarten names")
return
}
writeSuccess(c, http.StatusCreated, "create success", items[0])
}
// @Summary 更新用户信息
// @Description 更新指定用户的详细信息支持重置密码和吊销Token
// @Tags 用户管理
// @Accept json
// @Produce json
// @Param id path int true "用户ID"
// @Param user body userAdminPayload true "更新信息"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "更新成功"
// @Failure 400 {object} SwagAPIResponse "请求参数错误"
// @Failure 404 {object} SwagAPIResponse "用户不存在"
// @Router /admin/users/{id} [put]
func (uc *UserAdminController) Update(c *gin.Context) {
user, err := uc.findUserByID(c.Param("id"))
if err != nil {
respondUserLookupError(c, err)
return
}
var payload userAdminPayload
if err := c.ShouldBindJSON(&payload); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if err := validateUpdateUserPayload(payload); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if err := validateRegionIDs(payload.RegionIDs); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
user.Username = strings.TrimSpace(payload.Username)
user.Email = normalizeOptionalString(payload.Email)
user.Phone = normalizeOptionalString(payload.Phone)
user.Role = payload.Role
user.FlavorType = payload.FlavorType
if payload.IsActive != nil {
user.IsActive = *payload.IsActive
}
if strings.TrimSpace(payload.Password) != "" {
if err := user.HashPassword(payload.Password); err != nil {
writeError(c, http.StatusInternalServerError, "failed to hash password")
return
}
}
if err := uc.DB.Transaction(func(tx *gorm.DB) error {
revokeTokens := payload.RevokeTokens != nil && *payload.RevokeTokens
updateData := map[string]interface{}{
"username": user.Username,
"email": user.Email,
"phone": user.Phone,
"role": user.Role,
"flavor_type": user.FlavorType,
"is_active": user.IsActive,
}
if strings.TrimSpace(payload.Password) != "" {
updateData["password"] = user.Password
}
if revokeTokens {
updateData["token_invalid_before"] = time.Now().UnixMilli()
}
if err := tx.Model(&models.User{}).Where("id = ?", user.ID).Updates(updateData).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserRegionBinding{}).Error; err != nil {
return err
}
bindings := buildUserRegionBindings(payload.RegionIDs)
if len(bindings) > 0 {
for i := range bindings {
bindings[i].UserID = user.ID
}
if err := tx.Create(&bindings).Error; err != nil {
return err
}
}
return nil
}); err != nil {
writeUserAdminDBError(c, err)
return
}
if err := uc.DB.Preload("Regions").First(&user, user.ID).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to reload user")
return
}
items, err := uc.buildUserAdminItems([]models.User{user})
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to load kindergarten names")
return
}
writeSuccess(c, http.StatusOK, "update success", items[0])
}
// @Summary 删除用户
// @Description 删除指定用户不能删除admin账号和当前登录用户
// @Tags 用户管理
// @Produce json
// @Param id path int true "用户ID"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "删除成功"
// @Failure 400 {object} SwagAPIResponse "无法删除受保护账号"
// @Failure 404 {object} SwagAPIResponse "用户不存在"
// @Router /admin/users/{id} [delete]
func (uc *UserAdminController) Delete(c *gin.Context) {
user, err := uc.findUserByID(c.Param("id"))
if err != nil {
respondUserLookupError(c, err)
return
}
if isProtectedAdminAccount(user.Username) {
writeError(c, http.StatusBadRequest, "cannot delete protected admin user")
return
}
currentUserID, _, ok := currentUser(c)
if ok && currentUserID == user.ID {
writeError(c, http.StatusBadRequest, "cannot delete current user")
return
}
if err := uc.DB.Delete(&user).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to delete user")
return
}
writeSuccess(c, http.StatusOK, "delete success", nil)
}
func (uc *UserAdminController) findUserByID(id string) (models.User, error) {
var user models.User
numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64)
if err != nil {
return user, gorm.ErrRecordNotFound
}
if err := uc.DB.Preload("Regions").First(&user, numericID).Error; err != nil {
return user, err
}
return user, nil
}
func (uc *UserAdminController) buildUserAdminItems(users []models.User) ([]userAdminListItem, error) {
regionIDs := make([]uint32, 0)
seenRegions := make(map[uint32]struct{})
for _, user := range users {
for _, region := range user.Regions {
if _, exists := seenRegions[region.RegionID]; exists {
continue
}
seenRegions[region.RegionID] = struct{}{}
regionIDs = append(regionIDs, region.RegionID)
}
}
kindergartenMap := make(map[uint32]string)
if len(regionIDs) > 0 {
var kindergartens []models.Kindergarten
if err := uc.DB.Where("region_id IN ?", regionIDs).Find(&kindergartens).Error; err != nil {
return nil, err
}
for _, item := range kindergartens {
kindergartenMap[item.RegionID] = item.Name
}
}
items := make([]userAdminListItem, 0, len(users))
for _, user := range users {
item := userAdminListItem{
CreatedAt: user.CreatedAt,
Email: user.Email,
FlavorType: user.FlavorType,
ID: user.ID,
IsActive: user.IsActive,
Phone: user.Phone,
RegionIDs: user.RegionIDs(),
Regions: user.Regions,
Role: user.Role,
TokenInvalidBefore: user.TokenInvalidBefore,
UpdatedAt: user.UpdatedAt,
Username: user.Username,
}
for _, regionID := range item.RegionIDs {
if kindergartenName, exists := kindergartenMap[regionID]; exists {
item.KindergartenNames = append(item.KindergartenNames, kindergartenName)
}
}
items = append(items, item)
}
return items, nil
}
func validateCreateUserPayload(payload userAdminPayload) error {
if strings.TrimSpace(payload.Username) == "" {
return errors.New("username is required")
}
if strings.TrimSpace(payload.Password) == "" {
return errors.New("password is required")
}
return validateCommonUserPayload(payload)
}
func validateUpdateUserPayload(payload userAdminPayload) error {
if strings.TrimSpace(payload.Username) == "" {
return errors.New("username is required")
}
return validateCommonUserPayload(payload)
}
func validateCommonUserPayload(payload userAdminPayload) error {
if payload.Role == "" {
return errors.New("role is required")
}
if payload.FlavorType == "" {
return errors.New("flavorType is required")
}
return nil
}
func validateRegionIDs(regionIDs []uint32) error {
seen := make(map[uint32]struct{}, len(regionIDs))
for _, regionID := range regionIDs {
if regionID == 0 {
return errors.New("regionIds contains invalid value")
}
if _, exists := seen[regionID]; exists {
return errors.New("regionIds contains duplicate value")
}
seen[regionID] = struct{}{}
}
return nil
}
func normalizeOptionalString(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
}
func isProtectedAdminAccount(username string) bool {
return strings.EqualFold(strings.TrimSpace(username), "admin")
}
func respondUserLookupError(c *gin.Context, err error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(c, http.StatusNotFound, "user not found")
return
}
writeError(c, http.StatusInternalServerError, "failed to query user")
}
func writeUserAdminDBError(c *gin.Context, err error) {
if strings.Contains(strings.ToLower(err.Error()), "unique") {
writeError(c, http.StatusConflict, "username, email or phone already exists")
return
}
writeError(c, http.StatusInternalServerError, "failed to persist user")
}