Files

1299 lines
41 KiB
Go

// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <xxx@example.com>
// @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(&registerConfig)
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
}