// package system // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2025 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE package system import ( "context" "crypto/rand" "encoding/json" "errors" "fmt" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/bcache" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/smsgateway" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32" "math/big" "mime" "net" "net/smtp" "regexp" "slices" "strings" "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" "gorm.io/gorm" ) type sSysUser struct { } const ( registerCodeDailyLimit = 5 registerCodeTTL = 6 * time.Hour userBindCodeTTLSeconds = 300 userBindEmailCodeType = "email" userBindPhoneCodeType = "phone" ) func NewSysUser() *sSysUser { return &sSysUser{} } func init() { service.RegisterSysUser(NewSysUser()) } func (l *sSysUser) Model(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: true, }) } return handler.Model(dao.SysUser.Ctx(ctx), option...) } // 根据用户名获取用户信息,不验证当前租户 func (l *sSysUser) GetUserByUsername(ctx context.Context, username string, tenantId string) (user *entity.SysUser, err error) { var data *entity.SysUser err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, username).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&data) if err != nil { if err == gorm.ErrRecordNotFound { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } return nil, err } if data == nil { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } if data.Status == consts.SysUserStatusDisable { return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用") } if data.Status == consts.SysUserStatusLocked { return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定") } if data.Status == consts.SysUserStatusExpired { return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期") } if data.Status == consts.SysUserStatusDeleted { return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除") } return data, nil } // 根据邮箱获取用户信息,不验证当前租户 func (l *sSysUser) GetUserByEmail(ctx context.Context, email string, tenantId string) (user *entity.SysUser, err error) { err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Email, email).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user) if err != nil { if err == gorm.ErrRecordNotFound { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } return nil, err } if user == nil { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } if user.Status == consts.SysUserStatusDisable { return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用") } if user.Status == consts.SysUserStatusLocked { return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定") } if user.Status == consts.SysUserStatusExpired { return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期") } if user.Status == consts.SysUserStatusDeleted { return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除") } return user, nil } // 根据手机号获取用户信息,不验证当前租户 func (l *sSysUser) GetUserByPhone(ctx context.Context, phone string, tenantId string) (user *entity.SysUser, err error) { err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Phonenumber, phone).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user) if err != nil { if err == gorm.ErrRecordNotFound { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } return nil, err } if user == nil { return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } if user.Status == consts.SysUserStatusDisable { return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用") } if user.Status == consts.SysUserStatusLocked { return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定") } if user.Status == consts.SysUserStatusExpired { return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期") } if user.Status == consts.SysUserStatusDeleted { return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除") } return } // ListUsersByPhone 跨租户按手机号查询用户列表(登录场景:手机号可能对应多个租户下的账号)。 // 不校验状态,由调用方逐个处理,便于给出明确的错误提示。 func (l *sSysUser) ListUsersByPhone(ctx context.Context, phone string) (users []*entity.SysUser, err error) { err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Phonenumber, phone).Scan(&users) if err != nil { return nil, err } return users, nil } // 根据用户名和密码获取用户信息 func (l *sSysUser) GetUserByUsernameAndPassword(ctx context.Context, tenantId string, username string, password string) (user *entity.SysUser, err error) { // 判断用户名是否存在 user, err = l.GetUserByUsername(ctx, username, tenantId) if err != nil { return nil, err } // 判断密码是否正确 password = utility.PasswordEncrypt(password, user.Salt) if password != user.Password { g.Log().Errorf(ctx, "密码错误: username:%s, password:%s, user:%+v", username, password, user) return nil, gerror.NewCode(consts.CodeUserPasswordError, "密码错误") } return user, nil } // 根据用户ID获取用户信息 func (l *sSysUser) GetUserById(ctx context.Context, id int64) (user *model.SysUserViewModel, err error) { err = l.Model(ctx).WithAll().Where(dao.SysUser.Columns().UserId, id).Scan(&user) if err != nil { return nil, err } if user == nil { return nil, gerror.NewCode(consts.CodeUserNotFound, "用户不存在") } // 获取用户角色 userRoles := make([]*entity.SysUserRole, 0) err = dao.SysUserRole.Ctx(ctx). Where(dao.SysUserRole.Columns().TenantId, user.TenantId). Where(dao.SysUserRole.Columns().UserId, id).Scan(&userRoles) if err != nil { return nil, err } roleIds := make([]int64, 0) for _, userRole := range userRoles { roleIds = append(roleIds, userRole.RoleId) } roles := make([]*entity.SysRole, 0) if len(roleIds) > 0 { err = dao.SysRole.Ctx(ctx). Where(dao.SysRole.Columns().TenantId, user.TenantId). WhereIn(dao.SysRole.Columns().RoleId, roleIds).Scan(&roles) if err != nil { return nil, err } } user.Roles = make([]*model.SysRoleMiniModel, 0) for _, role := range roles { mini := &model.SysRoleMiniModel{} gconv.Struct(role, mini) user.Roles = append(user.Roles, mini) } // 获取用户岗位 userPosts := make([]*entity.SysUserPost, 0) err = dao.SysUserPost.Ctx(ctx). Where(dao.SysUserPost.Columns().TenantId, user.TenantId). Where(dao.SysUserPost.Columns().UserId, id).Scan(&userPosts) if err != nil { return nil, err } postIds := make([]int64, 0) for _, userPost := range userPosts { postIds = append(postIds, userPost.PostId) } posts := make([]*entity.SysPost, 0) if len(postIds) > 0 { err = dao.SysPost.Ctx(ctx). Where(dao.SysPost.Columns().TenantId, user.TenantId). WhereIn(dao.SysPost.Columns().PostId, postIds).Scan(&posts) if err != nil { return nil, err } } user.Posts = make([]*model.SysPostMiniModel, 0) for _, post := range posts { mini := &model.SysPostMiniModel{} gconv.Struct(post, mini) user.Posts = append(user.Posts, mini) } return } // 获取用户列表 func (l *sSysUser) List(ctx context.Context, page *request.PageInfo, query *model.UserListParam) (items []*model.SysUserListModel, total int, err error) { deptIds := make([]int64, 0) if query.DeptId != 0 { deptIds = append(deptIds, query.DeptId) subDeptIds, err := service.SysDept().GetDeptIdsByParentId(ctx, query.DeptId) if err != nil { return nil, 0, err } deptIds = append(deptIds, subDeptIds...) } g.Log().Infof(ctx, "sSysUser.List 获取部门列表: deptIds:%+v", deptIds) // 实现分页查询逻辑 var users []*model.SysUserListModel m := l.Model(ctx) if query.UserId != 0 { m = m.Where(dao.SysUser.Columns().UserId, query.UserId) } if len(deptIds) > 0 { m = m.WhereIn(dao.SysUser.Columns().DeptId, deptIds) } if query.UserName != "" { m = m.WhereLike(dao.SysUser.Columns().UserName, "%"+query.UserName+"%") } if query.NickName != "" { m = m.WhereLike(dao.SysUser.Columns().NickName, "%"+query.NickName+"%") } if query.Email != "" { m = m.WhereLike(dao.SysUser.Columns().Email, "%"+query.Email+"%") } if query.Phonenumber != "" { m = m.WhereLike(dao.SysUser.Columns().Phonenumber, "%"+query.Phonenumber+"%") } if query.Status != "" { m = m.Where(dao.SysUser.Columns().Status, query.Status) } if len(query.CreatedAt) == 2 { begin, end := gtime.NewFromStr(query.CreatedAt[0]), gtime.NewFromStr(query.CreatedAt[1]) m = m.WhereBetween(dao.SysUser.Columns().CreatedAt, begin, end.EndOfDay()) } if len(query.DeptIdList) > 0 { m = m.WhereIn(dao.SysUser.Columns().DeptId, query.DeptIdList) } total, err = m.Count() if err != nil { return nil, 0, err } err = m.WithAll().Page(page.Page, page.PageSize).Scan(&users) if err != nil { return nil, 0, err } // g.Log().Infof(ctx, "获取用户列表成功: total:%d, users:%+v", total, users) return users, total, nil } // 新增用户 func (l *sSysUser) AddUser(ctx context.Context, req *model.SysUserAddModel) (data *model.SysUserViewModel, err error) { // 获取当前登录用户的租户 if req.TenantId == "" { req.TenantId = contexts.GetTenantId(ctx) } // 判断部门是否存在 dept, err := service.SysDept().GetDeptById(ctx, req.DeptId) if err != nil { return nil, err } if dept == nil || dept.TenantId != req.TenantId { return nil, gerror.NewCode(consts.CodeDeptNotFound, "部门不存在") } if err := rolepolicy.ValidateUserRoleAssignment(ctx, req.TenantId, req.DeptId, req.RoleIds, nil); err != nil { return nil, err } // 判断用户名是否存在(在当前租户下) var user *entity.SysUser err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, req.UserName).Where(dao.SysUser.Columns().TenantId, req.TenantId).Scan(&user) if err != nil { if err != gorm.ErrRecordNotFound { return nil, err } } if user != nil { return nil, gerror.NewCode(consts.CodeUserExists, "用户名已存在") } // // 判断邮箱是否存在 // user, err = l.GetUserByEmail(ctx, req.Email) // if err != nil { // return nil, err // } // if user != nil { // return nil, gerror.NewCode(consts.CodeEmailExists, "邮箱已存在") // } // // 判断手机号是否存在 // user, err = l.GetUserByPhone(ctx, req.Phone) // if err != nil { // return nil, err // } // if user != nil { // return nil, gerror.NewCode(consts.CodePhoneExists, "手机号已存在") // } // 随机生成5位字符 salt := utility.RandomString(5) password := utility.PasswordEncrypt(req.Password, salt) dataInsert := do.SysUser{} gconv.Struct(req, &dataInsert) dataInsert.Password = password dataInsert.Salt = salt dataInsert.CreatedDept = contexts.GetDeptId(ctx) dataInsert.CreatedBy = contexts.GetUserId(ctx) dataInsert.CreatedAt = gtime.Now() dataInsert.UpdatedBy = contexts.GetUserId(ctx) dataInsert.UpdatedAt = gtime.Now() dataInsert.TenantId = req.TenantId // 新账号尚未设置密码,标记为「未由用户本人设置」,登录后首登引导修改 dataInsert.IsInitPassword = 1 if req.UserType == "" { dataInsert.UserType = consts.SysUserTypeSys } result, err := dao.SysUser.Ctx(ctx).Data(dataInsert).Insert() if err != nil { return nil, err } userId, err := result.LastInsertId() if err != nil { return nil, err } data, err = l.GetUserById(ctx, userId) if err != nil { return nil, err } // 新增用户角色 d := make([]map[string]any, 0) if len(req.RoleIds) > 0 { for _, roleId := range req.RoleIds { d = append(d, map[string]any{ dao.SysUserRole.Columns().TenantId: req.TenantId, dao.SysUserRole.Columns().UserId: userId, dao.SysUserRole.Columns().RoleId: roleId, }) } _, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert() if err != nil { return nil, err } } // 新增用户岗位 if len(req.PostIds) > 0 { d = make([]map[string]any, 0) for _, postId := range req.PostIds { d = append(d, map[string]any{ dao.SysUserPost.Columns().TenantId: req.TenantId, dao.SysUserPost.Columns().UserId: userId, dao.SysUserPost.Columns().PostId: postId, }) } _, err = dao.SysUserPost.Ctx(ctx).Data(d).Insert() if err != nil { return nil, err } } return data, nil } func (l *sSysUser) Profile(ctx context.Context) (user *model.UserProfileModel, err error) { u, err := l.GetUserById(ctx, contexts.GetUserId(ctx)) if err != nil { return nil, err } companyInfo := service.SysDept().GetParentCompanyInfo(ctx, u.DeptId) return &model.UserProfileModel{ SysUserViewModel: *u, CompanyInfo: &model.SysDeptMiniModel{ DeptId: companyInfo.DeptId, DeptName: companyInfo.DeptName, ParentId: companyInfo.ParentId, }, }, nil } func (l *sSysUser) UpdateCurrentUser(ctx context.Context, req *model.UpdateCurrentUserModel) (user *model.SysUserViewModel, err error) { userId := contexts.GetUserId(ctx) _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(req).OmitEmpty().Update() if err != nil { return nil, err } user, err = l.GetUserById(ctx, userId) if err != nil { return nil, err } event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId) return user, nil } func (l *sSysUser) UpdateCurrentUserPassword(ctx context.Context, req *model.UpdateCurrentUserPasswordModel) (err error) { userId := contexts.GetUserId(ctx) var user *entity.SysUser err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Scan(&user) if err != nil { return err } if user == nil { return gerror.NewCode(consts.CodeUserNotFound, "用户不存在") } oldPassword := utility.PasswordEncrypt(req.OldPassword, user.Salt) if oldPassword != user.Password { return gerror.NewCode(consts.CodeUserPasswordError, "旧密码错误") } salt := utility.RandomString(5) password := utility.PasswordEncrypt(req.NewPassword, salt) _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(map[string]any{ dao.SysUser.Columns().Password: password, dao.SysUser.Columns().Salt: salt, dao.SysUser.Columns().IsInitPassword: 0, dao.SysUser.Columns().UpdatedBy: userId, dao.SysUser.Columns().UpdatedAt: gtime.Now(), }).Update() if err != nil { return err } return nil } func (l *sSysUser) SendCurrentUserEmailCode(ctx context.Context, req *model.SendCurrentUserEmailCodeModel) (err error) { email := strings.TrimSpace(req.Email) if err = validateUserEmail(email); err != nil { return err } if err = verifyIdentityCaptcha(ctx, req.CaptchaID, req.CaptchaValue); err != nil { return err } user, err := currentEntityUser(ctx) if err != nil { return err } if strings.EqualFold(strings.TrimSpace(user.Email), email) { return gerror.New("新邮箱不能与当前邮箱相同") } if err = ensureEmailAvailable(ctx, user, email); err != nil { return err } code, err := GenerateMfaCode(ctx) if err != nil { return err } if err = setUserBindCode(ctx, userBindEmailCodeType, user.UserId, email, code); err != nil { return gerror.Wrap(err, "保存邮箱验证码失败,请稍后重试") } return sendUserEmailCode(ctx, email, code) } func (l *sSysUser) UpdateCurrentUserEmail(ctx context.Context, req *model.UpdateCurrentUserEmailModel) (err error) { email := strings.TrimSpace(req.Email) if err = validateUserEmail(email); err != nil { return err } user, err := currentEntityUser(ctx) if err != nil { return err } if err = verifyUserBindCode(ctx, userBindEmailCodeType, user.UserId, email, req.Code); err != nil { return err } if err = ensureEmailAvailable(ctx, user, email); err != nil { return err } _, err = dao.SysUser.Ctx(ctx). Where(dao.SysUser.Columns().UserId, user.UserId). Where(dao.SysUser.Columns().TenantId, user.TenantId). Data(g.Map{ dao.SysUser.Columns().Email: email, dao.SysUser.Columns().UpdatedBy: user.UserId, dao.SysUser.Columns().UpdatedAt: gtime.Now(), }). Update() if err != nil { return gerror.Wrap(err, "修改邮箱失败,请稍后重试") } _ = deleteUserBindCode(ctx, userBindEmailCodeType, user.UserId, email) event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, user.UserId) return nil } func (l *sSysUser) SendCurrentUserPhoneCode(ctx context.Context, req *model.SendCurrentUserPhoneCodeModel) (err error) { phone := strings.TrimSpace(req.Phonenumber) if err = validateUserPhone(phone); err != nil { return err } if err = verifyIdentityCaptcha(ctx, req.CaptchaID, req.CaptchaValue); err != nil { return err } user, err := currentEntityUser(ctx) if err != nil { return err } if strings.TrimSpace(user.Phonenumber) == phone { return gerror.New("新手机号不能与当前手机号相同") } if err = ensurePhoneAvailable(ctx, user, phone); err != nil { return err } code, err := GenerateMfaCode(ctx) if err != nil { return err } if err = setUserBindCode(ctx, userBindPhoneCodeType, user.UserId, phone, code); err != nil { return gerror.Wrap(err, "保存手机验证码失败,请稍后重试") } return sendUserPhoneCode(ctx, phone, code) } func (l *sSysUser) UpdateCurrentUserPhone(ctx context.Context, req *model.UpdateCurrentUserPhoneModel) (err error) { phone := strings.TrimSpace(req.Phonenumber) if err = validateUserPhone(phone); err != nil { return err } user, err := currentEntityUser(ctx) if err != nil { return err } if err = verifyUserBindCode(ctx, userBindPhoneCodeType, user.UserId, phone, req.Code); err != nil { return err } if err = ensurePhoneAvailable(ctx, user, phone); err != nil { return err } _, err = dao.SysUser.Ctx(ctx). Where(dao.SysUser.Columns().UserId, user.UserId). Where(dao.SysUser.Columns().TenantId, user.TenantId). Data(g.Map{ dao.SysUser.Columns().Phonenumber: phone, dao.SysUser.Columns().UpdatedBy: user.UserId, dao.SysUser.Columns().UpdatedAt: gtime.Now(), }). Update() if err != nil { return gerror.Wrap(err, "修改手机号失败,请稍后重试") } _ = deleteUserBindCode(ctx, userBindPhoneCodeType, user.UserId, phone) event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, user.UserId) return nil } func (l *sSysUser) UpdateUser(ctx context.Context, req *model.SysUserUpdateModel) (err error) { currUserId := contexts.GetUserId(ctx) var existsUser *entity.SysUser if err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, req.UserId).Scan(&existsUser); err != nil { return err } if existsUser == nil { return gerror.NewCode(consts.CodeUserNotFound, "用户不存在") } tenantId := existsUser.TenantId existingRoles, err := l.GetUserRoleIds(ctx, req.UserId) if err != nil { return err } deptID := existsUser.DeptId if req.DeptId != nil && *req.DeptId != deptID { deptID = *req.DeptId existingRoles = nil } if err := rolepolicy.ValidateUserRoleAssignment(ctx, tenantId, deptID, req.RoleIds, existingRoles); err != nil { return err } req.UpdatedAt = gtime.Now() req.UpdatedBy = &currUserId _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, req.UserId).Data(req).OmitNil().Update() if err != nil { return err } // 用户角色 urList := make([]*entity.SysUserRole, 0) err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().TenantId, tenantId).Where(dao.SysUserRole.Columns().UserId, req.UserId).Scan(&urList) if err != nil { return err } // 计算删除和新增的角色 delRoleIds := make([]int64, 0) addRoleIds := make([]int64, 0) hasRoleId := false for _, ur := range urList { hasRoleId = false for _, roleId := range req.RoleIds { if ur.RoleId == roleId { hasRoleId = true break } } if !hasRoleId { delRoleIds = append(delRoleIds, ur.RoleId) } } for _, roleId := range req.RoleIds { hasRoleId = false for _, ur := range urList { if ur.RoleId == roleId { hasRoleId = true break } } if !hasRoleId { addRoleIds = append(addRoleIds, roleId) } } // 删除多余的角色 if len(delRoleIds) > 0 { _, err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().TenantId, tenantId).Where(dao.SysUserRole.Columns().UserId, req.UserId).WhereIn(dao.SysUserRole.Columns().RoleId, delRoleIds).Delete() if err != nil { return err } } // 新增角色 d := make([]map[string]any, 0) if len(addRoleIds) > 0 { for _, roleId := range addRoleIds { d = append(d, map[string]any{ dao.SysUserRole.Columns().TenantId: tenantId, dao.SysUserRole.Columns().UserId: req.UserId, dao.SysUserRole.Columns().RoleId: roleId, }) } _, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert() if err != nil { return err } } // 查询用户岗位 upList := make([]*entity.SysUserPost, 0) err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().TenantId, tenantId).Where(dao.SysUserPost.Columns().UserId, req.UserId).Scan(&upList) if err != nil { return err } // 计算删除和新增的岗位 delPostIds := make([]int64, 0) addPostIds := make([]int64, 0) hasPostId := false for _, up := range upList { hasPostId = false for _, postId := range req.PostIds { if up.PostId == postId { hasPostId = true break } } if !hasPostId { delPostIds = append(delPostIds, up.PostId) } } for _, postId := range req.PostIds { hasPostId = false for _, up := range upList { if up.PostId == postId { hasPostId = true break } } if !hasPostId { addPostIds = append(addPostIds, postId) } } // 删除多余岗位 if len(delPostIds) > 0 { _, err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().TenantId, tenantId).Where(dao.SysUserPost.Columns().UserId, req.UserId).WhereIn(dao.SysUserPost.Columns().PostId, delPostIds).Delete() if err != nil { return err } } // 新增岗位 d = make([]map[string]any, 0) if len(addPostIds) > 0 { for _, postId := range addPostIds { d = append(d, map[string]any{ dao.SysUserPost.Columns().TenantId: tenantId, dao.SysUserPost.Columns().UserId: req.UserId, dao.SysUserPost.Columns().PostId: postId, }) } _, err = dao.SysUserPost.Ctx(ctx).Data(d).Insert() if err != nil { return err } } event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, req.UserId) return nil } func (l *sSysUser) DeleteUser(ctx context.Context, userIds []int64) (err error) { if len(userIds) == 0 { g.Log().Warningf(ctx, "sSysUser.DeleteUser 用户ID列表不能为空. userIds: %v", userIds) return errors.New("用户ID列表不能为空") } currUserId := contexts.GetUserId(ctx) if slices.Contains(userIds, 1) { g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除超级管理员. userIds: %v", userIds) return errors.New("不能删除超级管理员") } if slices.Contains(userIds, currUserId) { g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除当前用户. userIds: %v, currUserId: %d", userIds, currUserId) return errors.New("不能删除当前用户") } isAdminUserIds, err := service.SysTenant().IsTenantAdmin(ctx, userIds) if err != nil { g.Log().Errorf(ctx, "sSysUser.DeleteUser IsTenantAdmin err: %v, userIds: %v", err, userIds) return err } if len(isAdminUserIds) > 0 { g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除租户管理员. isAdminUserIds: %v, userIds: %v", isAdminUserIds, userIds) return errors.New(fmt.Sprintf("不能删除租户管理员ID: %v", isAdminUserIds)) } _, err = l.Model(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Data(map[string]any{ dao.SysUser.Columns().DeletedAt: gtime.Now(), dao.SysUser.Columns().DeletedBy: currUserId, }).OmitEmpty().Update() if err != nil { g.Log().Errorf(ctx, "sSysUser.DeleteUser Update err: %v, userIds: %v", err, userIds) return err } // 删除用户绑定关系 _, err = dao.SysSocial.Ctx(ctx).WhereIn(dao.SysSocial.Columns().UserId, userIds).Delete() if err != nil { g.Log().Errorf(ctx, "sSysUser.DeleteUser SysSocial.Ctx(ctx).Delete err: %v, userIds: %v", err, userIds) return err } event.EventsInstance().Emit(ctx, consts.EventKeyUserDelete, userIds) return nil } func (l *sSysUser) ResetPassword(ctx context.Context, userId int64, password string) (err error) { currUserId := contexts.GetUserId(ctx) salt := utility.RandomString(5) password = utility.PasswordEncrypt(password, salt) _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(map[string]any{ dao.SysUser.Columns().Password: password, dao.SysUser.Columns().Salt: salt, dao.SysUser.Columns().IsInitPassword: 1, dao.SysUser.Columns().UpdatedBy: currUserId, dao.SysUser.Columns().UpdatedAt: gtime.Now(), }).OmitEmpty().Update() if err != nil { return err } event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId) return nil } func (l *sSysUser) UpdateLoginInfo(ctx context.Context, userId int64, loginIp string) (err error) { _, err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(map[string]any{ dao.SysUser.Columns().LoginIp: loginIp, dao.SysUser.Columns().LoginDate: gtime.Now(), }).Update() if err != nil { g.Log().Errorf(ctx, "sSysUser.UpdateLoginInfo err: %v, userId: %d, loginIp: %s", err, userId, loginIp) return err } return nil } // 获取用户角色ID列表 func (l *sSysUser) GetUserRoleIds(ctx context.Context, userId int64) (roleIds []int64, err error) { urList := make([]*entity.SysUserRole, 0) tenantId := contexts.GetTenantId(ctx) db := dao.SysUserRole.Ctx(ctx).Master().Where(dao.SysUserRole.Columns().UserId, userId) if tenantId != "" { db = db.Where(dao.SysUserRole.Columns().TenantId, tenantId) } err = db.Scan(&urList) if err != nil { return nil, err } for _, ur := range urList { roleIds = append(roleIds, ur.RoleId) } return roleIds, nil } // 获取用户岗位ID列表 func (l *sSysUser) GetUserPostIds(ctx context.Context, userId int64) (postIds []int64, err error) { upList := make([]*entity.SysUserPost, 0) tenantId := contexts.GetTenantId(ctx) db := dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, userId) if tenantId != "" { db = db.Where(dao.SysUserPost.Columns().TenantId, tenantId) } err = db.Scan(&upList) if err != nil { return nil, err } for _, up := range upList { postIds = append(postIds, up.PostId) } return postIds, nil } func (l *sSysUser) Register(ctx context.Context, param *model.SysUserRegisterModel) (err error) { // 验证验证码 err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue) if err != nil { return err } // 检查是否开启用户注册功能 registerConfig := &model.SysConfigViewModel{} mod := g.DB().Model(dao.SysConfig.Table()) mod = mod.Where(dao.SysConfig.Columns().ConfigKey, "sys.account.registerUser") mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId) err = mod.Scan(®isterConfig) if err != nil { return gerror.New("请联系管理员开启用户注册功能") } if registerConfig == nil || registerConfig.ConfigValue != "true" { return gerror.New("请联系管理员开启用户注册功能") } // 获取租户信息 tenant, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{ TenantId: param.TenantId, }) if err != nil { return err } if tenant == nil { return gerror.New("租户无效") } // 获取租户的默认注册组织ID config := &model.SysConfigViewModel{} mod = g.DB().Model(dao.SysConfig.Table()) mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterDeptId) mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId) err = mod.Scan(&config) if err != nil { return gerror.New("该租户不允许自主注册") } if config == nil || config.ConfigValue == "" { return gerror.New("该租户不允许自主注册") } // 验证用户名是否存在 mod1 := g.DB().Model(dao.SysUser.Table()) mod1 = mod1.Where(dao.SysUser.Columns().UserName, param.UserName) mod1 = mod1.Where(dao.SysUser.Columns().TenantId, param.TenantId) total, err := mod1.Count() if err != nil { return err } if total > 0 { return gerror.New("用户名已存在") } // 随机生成5位字符 salt := utility.RandomString(5) password := utility.PasswordEncrypt(param.Password, salt) newUser := &entity.SysUser{ UserName: param.UserName, NickName: "", UserType: consts.SysUserTypeSys, Password: password, Salt: salt, TenantId: param.TenantId, DeptId: gconv.Int64(config.ConfigValue), Status: consts.SysDeptStatusNormal, CreatedBy: 0, CreatedAt: gtime.Now(), UpdatedBy: 0, UpdatedAt: gtime.Now(), } userId, err := l.Model(ctx).Data(newUser).InsertAndGetId() if err != nil { return err } //配置角色 // 新增用户角色 mod = g.DB().Model(dao.SysConfig.Table()) mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterRoleId) mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId) err = mod.Scan(&config) if err != nil { g.Log().Error(ctx, "获取租户的默认角色失败", err) } if config != nil && config.ConfigValue != "" { roleId := gconv.Int64(config.ConfigValue) d := map[string]any{ dao.SysUserRole.Columns().TenantId: param.TenantId, dao.SysUserRole.Columns().UserId: userId, dao.SysUserRole.Columns().RoleId: roleId, } _, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert() if err != nil { g.Log().Error(ctx, "获取租户的默认角色失败", err) } } return nil } // Status 更新用户状态 func (l *sSysUser) Status(ctx context.Context, param *model.SysUserStatusParam) (err error) { _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, param.UserId).Data(map[string]any{ dao.SysUser.Columns().Status: param.Status, }).OmitEmpty().Update() if err != nil { return err } return nil } // 批量查询用户迷你信息 func (l *sSysUser) BatchGetUserMiniInfo(ctx context.Context, userIds []int64) (users []*model.SysUserMiniModel, err error) { err = dao.SysUser.Ctx(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Scan(&users) if err != nil { return nil, err } return users, nil } // 登录时查看是否开启mfa验证 func (l *sSysUser) IsMfaEnabled(ctx context.Context) (mfaEnabledModel model.SysUserMfaEnabledModel, err error) { userId := contexts.GetUserId(ctx) // 查询用户是否开启MFA验证 err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId).Scan(&mfaEnabledModel) if err != nil { return model.SysUserMfaEnabledModel{}, err } return mfaEnabledModel, nil } // 是否开启 mfa验证 func (l *sSysUser) SetMfaEnabled(ctx context.Context) (uuid string, err error) { // 检查用户是否存在 user := &model.SysUserViewModel{} userId := contexts.GetUserId(ctx) err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId).Scan(&user) if err != nil { return "", err } // 检查用户邮箱是否匹配 if user.Email == "" { return "", gerror.New("邮箱不能为空,请先去个人中心,绑定邮箱再开启mfa验证") } uuid = uuid32.S() //给用户发邮件验证码 code, err := GenerateMfaCode(ctx) // 存储验证码到缓存 err = bcache.SetUserMfaCode(ctx, uuid, code) if err != nil { return "", err } g.Log().Info(ctx, "生成mfa验证码", code) return uuid, nil } // mfa验证 func (l *sSysUser) MfaVerify(ctx context.Context, param *model.SysUserIsMfaEnabledModel) (err error) { g.Log().Info(ctx, "mfa验证", param) userId := contexts.GetUserId(ctx) // 检查验证码是否匹配 code, err := bcache.GetUserMfaCode(ctx, param.Uuid) if err != nil { return err } if code != param.Code { return gerror.New("验证码错误") } // 删除验证码缓存 err = bcache.DeleteUserMfaCode(ctx, param.Uuid) if err != nil { return err } // 使用前端传过来的参数更新mfa状态 _, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(map[string]any{ dao.SysUser.Columns().MfaEnabled: param.MfaEnabled, }).Update() g.Log().Info(ctx, "mfa验证成功") return nil } func currentEntityUser(ctx context.Context) (user *entity.SysUser, err error) { userId := contexts.GetUserId(ctx) if userId <= 0 { return nil, gerror.New("当前用户无效") } mod := dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId) if tenantId := contexts.GetTenantId(ctx); tenantId != "" { mod = mod.Where(dao.SysUser.Columns().TenantId, tenantId) } if err = mod.Scan(&user); err != nil { return nil, err } if user == nil { return nil, gerror.NewCode(consts.CodeUserNotFound, "用户不存在") } return user, nil } func verifyIdentityCaptcha(ctx context.Context, captchaId string, captchaValue string) error { if strings.TrimSpace(captchaId) == "" || strings.TrimSpace(captchaValue) == "" { return gerror.New("身份验证码不能为空") } return service.SysCaptcha().VerifyCaptcha(ctx, captchaId, captchaValue) } func sendUserEmailCode(ctx context.Context, email string, code string) error { var cfg model.UserCodeEmailSenderConfig if v, err := g.Cfg().Get(ctx, "register.email"); err == nil && !v.IsNil() { _ = v.Scan(&cfg) } if cfg.Url != "" { return callUserCodeHTTP(ctx, cfg.UserCodeHttpSenderConfig, email, code) } if cfg.Host != "" { return sendUserCodeBySMTP(ctx, cfg, email, code) } g.Log().Infof(ctx, "user email bind code, email: %s, code: %s", email, code) return nil } func sendUserPhoneCode(ctx context.Context, phone string, code string) error { sent, err := smsgateway.Send(ctx, smsgateway.Request{ Scene: smsgateway.SceneBindPhone, PhoneNumber: phone, MsgContent: code, }) if err != nil { return err } if !sent { // 短信网关未启用(例如开发环境尚未部署 iotlcm-sms)时打印验证码,便于本地联调。 g.Log().Infof(ctx, "user phone bind code, phone: %s, code: %s", phone, code) } return nil } func callUserCodeHTTP(ctx context.Context, cfg model.UserCodeHttpSenderConfig, contact string, code string) error { method := strings.ToUpper(strings.TrimSpace(cfg.Method)) if method == "" { method = "POST" } successStatus := cfg.SuccessStatus if successStatus <= 0 { successStatus = 200 } body := renderUserCodeTemplate(cfg.BodyTemplate, contact, code) if body == "" { bodyBytes, _ := json.Marshal(g.Map{ "contact": contact, "code": code, }) body = string(bodyBytes) } headers := cfg.Headers if headers == nil { headers = map[string]string{} } if _, ok := headers["Content-Type"]; !ok { headers["Content-Type"] = "application/json" } resp, err := g.Client().Timeout(utility.ExternalRequestTimeout(cfg.Timeout)).SetHeaderMap(headers).DoRequest(ctx, method, cfg.Url, body) if err != nil { return gerror.Wrap(err, "发送验证码失败,请稍后重试") } defer resp.Close() if resp.StatusCode != successStatus { return gerror.Newf("发送验证码失败,服务响应状态码:%d", resp.StatusCode) } return nil } func sendUserCodeBySMTP(ctx context.Context, cfg model.UserCodeEmailSenderConfig, email string, code string) error { if cfg.Port <= 0 { cfg.Port = 25 } if cfg.From == "" { cfg.From = cfg.Username } addr := net.JoinHostPort(cfg.Host, gconv.String(cfg.Port)) auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) msg := buildUserCodeEmailMessage(cfg, email, code) err := utility.SendSMTP(ctx, utility.SMTPOptions{ Address: addr, Host: cfg.Host, Auth: auth, From: cfg.From, To: []string{email}, Data: msg, SSL: cfg.SSL, Timeout: utility.ExternalRequestTimeout(cfg.Timeout), }) if err != nil { g.Log().Errorf(ctx, "send user email code failed, email: %s, err: %+v", email, err) return gerror.New("发送邮箱验证码失败,请稍后重试") } return nil } func buildUserCodeEmailMessage(cfg model.UserCodeEmailSenderConfig, email string, code string) []byte { subject := cfg.Subject if subject == "" { subject = "邮箱验证码" } body := renderUserCodeTemplate(cfg.BodyTemplate, email, code) if body == "" { body = fmt.Sprintf("您的验证码是:%s,5分钟内有效。", code) } from := cfg.From if cfg.FromName != "" { from = mime.QEncoding.Encode("UTF-8", cfg.FromName) + " <" + cfg.From + ">" } message := strings.Builder{} message.WriteString("From: " + from + "\r\n") message.WriteString("To: " + email + "\r\n") message.WriteString("Subject: " + mime.QEncoding.Encode("UTF-8", subject) + "\r\n") message.WriteString("MIME-Version: 1.0\r\n") message.WriteString("Content-Type: text/html; charset=UTF-8\r\n") message.WriteString("\r\n") message.WriteString(body) return []byte(message.String()) } func renderUserCodeTemplate(template string, contact string, code string) string { if template == "" { return "" } replacer := strings.NewReplacer( "{contact}", contact, "{{contact}}", contact, "{email}", contact, "{{email}}", contact, "{phone}", contact, "{{phone}}", contact, "{code}", code, "{{code}}", code, ) return replacer.Replace(template) } func ensureEmailAvailable(ctx context.Context, current *entity.SysUser, email string) error { count, err := dao.SysUser.Ctx(ctx). Where(dao.SysUser.Columns().TenantId, current.TenantId). Where(dao.SysUser.Columns().Email, email). WhereNot(dao.SysUser.Columns().UserId, current.UserId). Count() if err != nil { return err } if count > 0 { return gerror.New("邮箱已被使用") } return nil } func ensurePhoneAvailable(ctx context.Context, current *entity.SysUser, phone string) error { count, err := dao.SysUser.Ctx(ctx). Where(dao.SysUser.Columns().TenantId, current.TenantId). Where(dao.SysUser.Columns().Phonenumber, phone). WhereNot(dao.SysUser.Columns().UserId, current.UserId). Count() if err != nil { return err } if count > 0 { return gerror.New("手机号已被使用") } return nil } func validateUserEmail(email string) error { if email == "" { return gerror.New("邮箱不能为空") } if ok, _ := regexp.MatchString(`^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$`, email); !ok { return gerror.New("邮箱格式不正确") } return nil } func validateUserPhone(phone string) error { if phone == "" { return gerror.New("手机号不能为空") } if ok, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone); !ok { return gerror.New("手机号格式不正确") } return nil } func userBindCodeKey(codeType string, userId int64, contact string) string { return fmt.Sprintf("user:profile:%s:%d:%s", codeType, userId, strings.TrimSpace(contact)) } func setUserBindCode(ctx context.Context, codeType string, userId int64, contact string, code string) error { _, err := g.Redis().Do(ctx, "SETEX", userBindCodeKey(codeType, userId, contact), userBindCodeTTLSeconds, code) return err } func getUserBindCode(ctx context.Context, codeType string, userId int64, contact string) (string, error) { value, err := g.Redis().Get(ctx, userBindCodeKey(codeType, userId, contact)) if err != nil { return "", err } if value.IsNil() { return "", nil } return value.String(), nil } func deleteUserBindCode(ctx context.Context, codeType string, userId int64, contact string) error { _, err := g.Redis().Del(ctx, userBindCodeKey(codeType, userId, contact)) return err } func verifyUserBindCode(ctx context.Context, codeType string, userId int64, contact string, code string) error { code = strings.TrimSpace(code) if code == "" { return gerror.New("验证码不能为空") } cacheCode, err := getUserBindCode(ctx, codeType, userId, contact) if err != nil { return gerror.Wrap(err, "获取验证码失败,请稍后重试") } if cacheCode == "" { return gerror.New("验证码已失效") } if cacheCode != code { return gerror.New("验证码错误") } return nil } // GenerateMfaCode 生成 6 位数字验证码。 // 使用 crypto/rand 均匀随机并保留前导零;旧实现(Unix 时间戳取模)可被预测, // 验证码又用于登录等未鉴权场景,属可枚举凭据,必须随机。 func GenerateMfaCode(ctx context.Context) (code string, err error) { n, err := rand.Int(rand.Reader, big.NewInt(1000000)) if err != nil { return "", gerror.Wrap(err, "生成验证码失败,请稍后重试") } return fmt.Sprintf("%06d", n.Int64()), nil }