package system import ( "context" "encoding/json" "errors" "strconv" "strings" "time" "kra/app/system/biz" "kra/pkg/database/pagination" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) { var po userPO if err := r.data.DB().WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, biz.ErrUserNotFound } return nil, err } return r.loadUser(ctx, &po) } func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) { var po userPO if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, biz.ErrUserNotFound } return nil, err } return r.loadUser(ctx, &po) } func (r *userRepo) FindUserByUUID(ctx context.Context, value string) (*biz.User, error) { var po userPO if err := r.data.DB().WithContext(ctx).Where("uuid = ?", value).First(&po).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, biz.ErrUserNotFound } return nil, err } return r.loadUser(ctx, &po) } func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error) { var authority authorityPO if err := r.data.DB().WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } var authorityPOs []authorityPO if err := r.data.DB().WithContext(ctx).Table("sys_authorities").Joins("JOIN sys_user_authority ON sys_user_authority.sys_authority_authority_id = sys_authorities.authority_id").Where("sys_user_authority.sys_user_id = ? AND sys_authorities.deleted_at IS NULL", po.ID).Find(&authorityPOs).Error; err != nil { return nil, err } authorities := make([]biz.Authority, 0, len(authorityPOs)) for _, item := range authorityPOs { authorities = append(authorities, toBizAuthority(item)) } setting := map[string]any(nil) if po.OriginSetting != "" { _ = json.Unmarshal([]byte(po.OriginSetting), &setting) } var departmentPOs []departmentPO if err := r.data.DB().WithContext(ctx).Table("sys_departments").Joins("JOIN sys_user_departments ON sys_user_departments.sys_department_id = sys_departments.id").Where("sys_user_departments.sys_user_id = ? AND sys_departments.deleted_at IS NULL", po.ID).Find(&departmentPOs).Error; err != nil { return nil, err } departments := make([]biz.Department, 0, len(departmentPOs)) for _, item := range departmentPOs { value := deptFromPO(item) departments = append(departments, *value) } var primary *biz.Department if po.DeptID != 0 { var primaryPO departmentPO if err := r.data.DB().WithContext(ctx).First(&primaryPO, po.DeptID).Error; err == nil { primary = deptFromPO(primaryPO) } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } } var positionPOs []positionPO if err := r.data.DB().WithContext(ctx).Table("sys_positions").Joins("JOIN sys_user_positions ON sys_user_positions.sys_position_id = sys_positions.id").Where("sys_user_positions.sys_user_id = ? AND sys_positions.deleted_at IS NULL", po.ID).Find(&positionPOs).Error; err != nil { return nil, err } positions := make([]biz.Position, 0, len(positionPOs)) for _, item := range positionPOs { positions = append(positions, *posFromPO(item)) } return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil } func toBizAuthority(po authorityPO) biz.Authority { var deletedAt *time.Time if po.DeletedAt.Valid { value := po.DeletedAt.Time deletedAt = &value } return biz.Authority{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, AuthorityID: po.AuthorityID, AuthorityName: po.AuthorityName, ParentID: po.ParentID, DataScope: po.DataScope, DefaultRouter: po.DefaultRouter} } func baseBizUser(po *userPO) *biz.User { setting := map[string]any(nil) if po.OriginSetting != "" { _ = json.Unmarshal([]byte(po.OriginSetting), &setting) } return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt} } func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) { if len(pos) == 0 { return []*biz.User{}, nil } db := r.data.DB().WithContext(ctx) userIDs := make([]uint, 0, len(pos)) authorityIDs := make([]uint, 0, len(pos)) departmentIDs := make([]uint, 0, len(pos)) for i := range pos { userIDs = append(userIDs, pos[i].ID) authorityIDs = append(authorityIDs, pos[i].AuthorityID) if pos[i].DeptID != 0 { departmentIDs = append(departmentIDs, pos[i].DeptID) } } var authorityLinks []userAuthorityPO if err := db.Where("sys_user_id IN ?", userIDs).Find(&authorityLinks).Error; err != nil { return nil, err } authorityByUser := make(map[uint][]uint, len(pos)) for _, link := range authorityLinks { authorityByUser[link.SysUserID] = append(authorityByUser[link.SysUserID], link.SysAuthorityAuthorityID) authorityIDs = append(authorityIDs, link.SysAuthorityAuthorityID) } var authorityPOs []authorityPO if err := db.Where("authority_id IN ?", authorityIDs).Find(&authorityPOs).Error; err != nil { return nil, err } authorityByID := make(map[uint]authorityPO, len(authorityPOs)) for _, item := range authorityPOs { authorityByID[item.AuthorityID] = item } var departmentLinks []userDepartmentPO if err := db.Where("sys_user_id IN ?", userIDs).Find(&departmentLinks).Error; err != nil { return nil, err } departmentByUser := make(map[uint][]uint, len(pos)) for _, link := range departmentLinks { departmentByUser[link.UserID] = append(departmentByUser[link.UserID], link.DepartmentID) departmentIDs = append(departmentIDs, link.DepartmentID) } var departmentPOs []departmentPO if len(departmentIDs) > 0 { if err := db.Where("id IN ?", departmentIDs).Find(&departmentPOs).Error; err != nil { return nil, err } } departmentByID := make(map[uint]departmentPO, len(departmentPOs)) for _, item := range departmentPOs { departmentByID[item.ID] = item } var positionLinks []userPositionPO if err := db.Where("sys_user_id IN ?", userIDs).Find(&positionLinks).Error; err != nil { return nil, err } positionByUser := make(map[uint][]uint, len(pos)) positionIDs := make([]uint, 0, len(positionLinks)) for _, link := range positionLinks { positionByUser[link.UserID] = append(positionByUser[link.UserID], link.PositionID) positionIDs = append(positionIDs, link.PositionID) } var positionPOs []positionPO if len(positionIDs) > 0 { if err := db.Where("id IN ?", positionIDs).Find(&positionPOs).Error; err != nil { return nil, err } } positionByID := make(map[uint]positionPO, len(positionPOs)) for _, item := range positionPOs { positionByID[item.ID] = item } users := make([]*biz.User, 0, len(pos)) for i := range pos { po := &pos[i] user := baseBizUser(po) user.Authority = toBizAuthority(authorityByID[po.AuthorityID]) user.Authorities = make([]biz.Authority, 0, len(authorityByUser[po.ID])) for _, id := range authorityByUser[po.ID] { if item, ok := authorityByID[id]; ok { user.Authorities = append(user.Authorities, toBizAuthority(item)) } } user.Departments = make([]biz.Department, 0, len(departmentByUser[po.ID])) for _, id := range departmentByUser[po.ID] { if item, ok := departmentByID[id]; ok { user.Departments = append(user.Departments, *deptFromPO(item)) } } if primary, ok := departmentByID[po.DeptID]; ok { user.Department = deptFromPO(primary) } user.Positions = make([]biz.Position, 0, len(positionByUser[po.ID])) for _, id := range positionByUser[po.ID] { if item, ok := positionByID[id]; ok { user.Positions = append(user.Positions, *posFromPO(item)) } } users = append(users, user) } return users, nil } func (r *userRepo) HasAuthorityMenu(ctx context.Context, authorityID uint, name string) (bool, error) { var count int64 err := r.data.DB().WithContext(ctx).Model(&menuPO{}). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, name). Count(&count).Error return count > 0, err } func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) { var pos []menuPO err := r.data.DB().WithContext(ctx).Table("sys_base_menus"). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.deleted_at IS NULL", authorityID). Order("sys_base_menus.sort").Scan(&pos).Error if err != nil { return nil, err } byID := make(map[uint]*biz.Menu, len(pos)) menuIDs := make([]uint, 0, len(pos)) for _, po := range pos { menu := menuFromPO(po) menu.MenuID = po.ID menu.Children = nil menu.Parameters = []*biz.MenuParameter{} menu.Buttons = nil byID[po.ID] = menu menuIDs = append(menuIDs, po.ID) } if len(menuIDs) > 0 { var parameters []menuParameterPO if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", menuIDs).Find(¶meters).Error; err != nil { return nil, err } for _, parameter := range parameters { menu := byID[parameter.MenuID] menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter)) } var buttons []menuButtonPO if err := r.data.DB().WithContext(ctx).Table("sys_base_menu_btns").Joins("JOIN sys_authority_btns ON sys_authority_btns.sys_base_menu_btn_id = sys_base_menu_btns.id").Where("sys_authority_btns.authority_id = ? AND sys_authority_btns.sys_menu_id IN ?", authorityID, menuIDs).Find(&buttons).Error; err != nil { return nil, err } for _, button := range buttons { menu := byID[button.MenuID] value := menuButtonFromPO(button) value.AuthorityID = authorityID menu.Buttons = append(menu.Buttons, value) } } var roots []*biz.Menu for _, po := range pos { menu := byID[po.ID] if parent := byID[po.ParentID]; parent != nil { parent.Children = append(parent.Children, menu) } else if po.ParentID == 0 { roots = append(roots, menu) } } return roots, nil } func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) { db := r.data.DB().WithContext(ctx).Model(&userPO{}) order := "id desc" if filter != nil { if filter.Username != "" { db = db.Where("username LIKE ?", "%"+filter.Username+"%") } if filter.NickName != "" { db = db.Where("nick_name LIKE ?", "%"+filter.NickName+"%") } if filter.Phone != "" { db = db.Where("phone LIKE ?", "%"+filter.Phone+"%") } if filter.Email != "" { db = db.Where("email LIKE ?", "%"+filter.Email+"%") } allowed := map[string]bool{"id": true, "username": true, "nick_name": true, "phone": true, "email": true} if allowed[filter.OrderKey] { order = filter.OrderKey if filter.Desc { order += " desc" } } } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var pos []userPO if err := pagination.ApplyRequired(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil { return nil, 0, err } users, err := r.loadUsers(ctx, pos) return users, total, err } func (r *userRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) { return r.CreateUserWithAuthorities(ctx, user, nil) } func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) { requestedAuthorities := append([]uint{user.AuthorityID}, authorityIDs...) if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDsAuth(ctx, requestedAuthorities); err != nil { return nil, err } if user.UUID == "" { user.UUID = uuid.NewString() } if user.Enable == 0 { user.Enable = 1 } if user.AuthorityID == 0 { user.AuthorityID = 888 } if user.NickName == "" { user.NickName = "系统用户" } now := time.Now() po := userPO{UUID: user.UUID, Username: user.Username, Password: user.Password, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Phone: user.Phone, Email: user.Email, Enable: user.Enable, PasswordUpdatedAt: &now, MustChangePassword: user.MustChangePassword} var createdAuthorities []biz.Authority if err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { // The compatible registration rejects an existing non-deleted username before creating the // account. The username column is intentionally not unique for legacy // schema compatibility, so keep the behavioral check here rather than // relying on a database constraint. var existing userPO if err := tx.Where("username = ?", user.Username).First(&existing).Error; err == nil { return errors.New("用户名已注册") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return err } if err := tx.Create(&po).Error; err != nil { return err } if len(authorityIDs) == 0 { return nil } links := make([]userAuthorityPO, 0, len(authorityIDs)) for _, authorityID := range authorityIDs { // Creating a user through the compatible administration endpoint lets // GORM save the many-to-many authority values supplied by the request. // With an ID-only value this is an upsert: an existing role is left // untouched, while a missing role is materialized with model defaults. associated := authorityPO{AuthorityID: authorityID} if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&associated).Error; err != nil { return err } createdAuthorities = append(createdAuthorities, toBizAuthority(associated)) links = append(links, userAuthorityPO{SysUserID: po.ID, SysAuthorityAuthorityID: authorityID}) } return tx.Create(&links).Error }); err != nil { return nil, err } // Registration returns the just-created entity, not a second hydrated // query. The reference handler returns the request's authority IDs and // leaves related authority/department records at their zero values; keep // that response contract while retaining the persisted timestamps/UUID. created := baseBizUser(&po) created.Authorities = createdAuthorities return created, nil } func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) error { if err := r.checkUserUpdateAuth(ctx, user); err != nil { return err } return r.updateUser(r.data.DB().WithContext(ctx), user) } func (r *userRepo) checkUserUpdateAuth(ctx context.Context, user *biz.User) error { access := &authorityAccessRepo{data: r.data} if err := access.checkUserIDAuth(ctx, user.ID, false); err != nil { return err } if user.AuthorityID != 0 { return access.checkAuthorityIDAuth(ctx, user.AuthorityID) } return nil } func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error { updates := make(map[string]any) if user.NickName != "" { updates["nick_name"] = user.NickName } if user.HeaderImg != "" { updates["header_img"] = user.HeaderImg } if user.Phone != "" { updates["phone"] = user.Phone } if user.Email != "" { updates["email"] = user.Email } if user.Enable != 0 { updates["enable"] = user.Enable } if len(updates) == 0 { return nil } return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error } func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error { if user == nil || len(user.Departments) == 0 { return nil } idSet := make(map[uint]bool) for i := range user.Departments { for _, part := range strings.Split(user.Departments[i].Ancestors, ",") { id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64) if err == nil && id != 0 { idSet[uint(id)] = true } } } ids := make([]uint, 0, len(idSet)) for id := range idSet { ids = append(ids, id) } nameByID := make(map[uint]string, len(ids)) if len(ids) > 0 { var rows []departmentPO if err := r.data.DB().WithContext(ctx).Select("id", "name").Where("id IN ?", ids).Find(&rows).Error; err != nil { return err } for _, row := range rows { nameByID[row.ID] = row.Name } } for i := range user.Departments { parts := make([]string, 0) for _, part := range strings.Split(user.Departments[i].Ancestors, ",") { id, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64) if err == nil && id != 0 && nameByID[uint(id)] != "" { parts = append(parts, nameByID[uint(id)]) } } parts = append(parts, user.Departments[i].Name) user.Departments[i].NamePath = strings.Join(parts, "/") } return nil } func (r *userRepo) updateUser(tx *gorm.DB, user *biz.User) error { updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable} if user.AuthorityID != 0 { updates["authority_id"] = user.AuthorityID } return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error } func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error { access := &authorityAccessRepo{data: r.data} if err := r.checkUserUpdateAuth(ctx, user); err != nil { return err } if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil { return err } return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := r.updateUser(tx, user); err != nil { return err } if len(authorityIDs) == 0 { return nil } return setUserAuthorities(tx, user.ID, authorityIDs) }) } func (r *userRepo) DeleteUser(ctx context.Context, id uint) error { if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, false); err != nil { return err } return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("id = ?", id).Delete(&userPO{}).Error; err != nil { return err } if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { return err } if err := tx.Where("sys_user_id = ?", id).Delete(&userDepartmentPO{}).Error; err != nil { return err } if err := tx.Where("sys_user_id = ?", id).Delete(&userPositionPO{}).Error; err != nil { return err } return nil }) } func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error { if err := (&authorityAccessRepo{data: r.data}).checkUserIDAuth(ctx, id, true); err != nil { return err } now := time.Now() updates := map[string]any{"password": password, "password_updated_at": now} if clearMustChange { updates["must_change_password"] = false } return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error } func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { var pos []authorityPO if err := r.data.DB().WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil { return nil, err } result := make([]*biz.Authority, 0, len(pos)) for _, po := range pos { a := toBizAuthority(po) result = append(result, &a) } return result, nil } func (r *userRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error { access := &authorityAccessRepo{data: r.data} if err := access.checkUserIDAuth(ctx, id, false); err != nil { return err } if err := access.checkAuthorityIDsAuth(ctx, authorityIDs); err != nil { return err } return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { var user userPO if err := tx.Where("id = ?", id).First(&user).Error; err != nil { return errors.New("查询用户数据失败") } return setUserAuthorities(tx, id, authorityIDs) }) } func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error { if len(authorityIDs) == 0 { return errors.New("角色不能为空") } if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { return err } links := make([]userAuthorityPO, 0, len(authorityIDs)) for _, authorityID := range authorityIDs { links = append(links, userAuthorityPO{SysUserID: id, SysAuthorityAuthorityID: authorityID}) } if err := tx.Create(&links).Error; err != nil { return err } return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error } func (r *userRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error { db := r.data.DB().WithContext(ctx) var count int64 if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil { return err } if count == 0 { return errors.New("该用户无此角色") } var authority authorityPO if err := db.Where("authority_id = ?", authorityID).First(&authority).Error; err != nil { return err } var menuCount int64 if err := db.Model(&menuPO{}). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). Where("sys_authority_menus.sys_authority_authority_id = ? AND sys_base_menus.name = ?", authorityID, authority.DefaultRouter). Count(&menuCount).Error; err != nil { return err } if menuCount == 0 { return errors.New("找不到默认路由,无法切换本角色") } return db.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error } func (r *userRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error { value, err := json.Marshal(setting) if err != nil { return err } return r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Update("origin_setting", string(value)).Error } // ActiveName temporarily carries the parent menu name during seeding. It is // cleared from responses by the service for these records.