1299 lines
41 KiB
Go
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(®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
|
|
}
|