// Package sys // @Link https://github.com/bufanyun/hotgo // @Copyright Copyright (c) 2026 HotGo // @Author Ms <133814250@qq.com> // @License https://github.com/bufanyun/hotgo/blob/master/LICENSE package sys import ( "context" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "hotgo/internal/consts" "hotgo/internal/dao" "hotgo/internal/library/contexts" "hotgo/internal/library/imadmin" "hotgo/internal/library/imtenant" "hotgo/internal/model/entity" "hotgo/internal/model/input/sysin" "hotgo/internal/service" "hotgo/utility/excel" "io" "net" "net/url" "strconv" "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/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/guid" "github.com/xuri/excelize/v2" "golang.org/x/crypto/bcrypt" ) type sSysImUser struct{} func NewSysImUser() *sSysImUser { return &sSysImUser{} } func init() { service.RegisterSysImUser(NewSysImUser()) } func (s *sSysImUser) Create(ctx context.Context, in *sysin.ImUserCreateInp) (res *sysin.ImUserCreateModel, err error) { if err = in.Filter(ctx); err != nil { return nil, err } tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } if in.GroupId > 0 { if err = validateImUserPolicyGroup(ctx, tenant.Id, in.GroupId); err != nil { return nil, err } } payload := map[string]any{ "username": in.Username, "nickname": in.Nickname, "phone": in.Phone, "password": in.Password, } var wire struct { DisplayNo int `json:"display_no"` UserId int64 `json:"user_id"` Username string `json:"username"` Nickname string `json:"nickname"` Phone string `json:"phone"` } path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users" if err = imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, &wire); err != nil { return nil, err } res = &sysin.ImUserCreateModel{DisplayNo: wire.DisplayNo, UserId: wire.UserId, Username: wire.Username, Nickname: wire.Nickname, Phone: wire.Phone} if in.GroupId > 0 { if err = service.SysImUserPolicyGroup().MemberAdd(ctx, &sysin.ImUserPolicyGroupMemberAddInp{GroupId: in.GroupId, UserId: res.UserId}); err != nil { return nil, gerror.Wrap(err, "用户已创建,但加入用户分组失败") } } _ = writeImAdminAudit(ctx, tenant.Id, "im_user_create", "user", res.UserId, "success", map[string]any{"username": res.Username, "group_id": in.GroupId}) return res, nil } func (s *sSysImUser) BatchCreate(ctx context.Context, in *sysin.ImUserBatchCreateInp) (res *sysin.ImUserBatchCreateModel, err error) { if err = in.Filter(ctx); err != nil { return nil, err } rows := make([]*sysin.ImUserCreateInp, 0, in.Count) for i := 1; i <= in.Count; i++ { nickname := in.NicknamePrefix if nickname == "" { nickname = "用户" } rows = append(rows, &sysin.ImUserCreateInp{TenantId: in.TenantId, Nickname: fmt.Sprintf("%s%d", nickname, i), Password: in.Password, GroupId: in.GroupId}) } return s.Import(ctx, &sysin.ImUserImportInp{TenantId: in.TenantId, GroupId: in.GroupId, Rows: rows}) } func (s *sSysImUser) Import(ctx context.Context, in *sysin.ImUserImportInp) (res *sysin.ImUserBatchCreateModel, err error) { if len(in.Rows) == 0 { return nil, gerror.New("没有可导入的用户数据") } if len(in.Rows) > 500 { return nil, gerror.New("单次最多导入500个用户") } res = &sysin.ImUserBatchCreateModel{Items: make([]*sysin.ImUserBatchItemModel, 0, len(in.Rows))} for index, row := range in.Rows { row.TenantId = in.TenantId if row.GroupId == 0 { row.GroupId = in.GroupId } item := &sysin.ImUserBatchItemModel{Row: index + 2, Username: row.Username} created, createErr := s.Create(ctx, row) if createErr != nil { item.Reason = createErr.Error() res.FailedCount++ } else { item.Success = true item.DisplayNo = created.DisplayNo item.UserId = created.UserId item.Username = created.Username res.SuccessCount++ } res.Items = append(res.Items, item) } return res, nil } func (s *sSysImUser) ImportExcel(ctx context.Context, tenantId, groupId int64, reader io.Reader) (res *sysin.ImUserBatchCreateModel, err error) { workbook, err := excelize.OpenReader(reader) if err != nil { return nil, gerror.New("Excel文件格式不正确") } defer workbook.Close() sheets := workbook.GetSheetList() if len(sheets) == 0 { return nil, gerror.New("Excel文件没有工作表") } rows, err := workbook.GetRows(sheets[0]) if err != nil { return nil, gerror.Wrap(err, "读取Excel文件失败") } input := &sysin.ImUserImportInp{TenantId: tenantId, GroupId: groupId} for index, row := range rows { if index == 0 || len(row) == 0 { continue } cell := func(i int) string { if i >= len(row) { return "" } return strings.TrimSpace(row[i]) } rowGroupId := groupId if raw := cell(4); raw != "" { if _, scanErr := fmt.Sscan(raw, &rowGroupId); scanErr != nil { rowGroupId = groupId } } input.Rows = append(input.Rows, &sysin.ImUserCreateInp{ TenantId: tenantId, Username: cell(0), Nickname: cell(1), Phone: cell(2), Password: cell(3), GroupId: rowGroupId, }) } return s.Import(ctx, input) } func (s *sSysImUser) DownloadTemplate(ctx context.Context) error { type templateRow struct { Username string Nickname string Phone string Password string GroupId string } return excel.ExportByStructs(ctx, []string{"用户名(可空,自动生成)", "昵称(必填)", "手机号", "密码(必填)", "用户分组ID"}, []templateRow{{Username: "", Nickname: "示例用户", Phone: "", Password: "123456", GroupId: ""}}, "用户导入模板", "用户") } // List 获取IM用户列表 func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) { if err = in.Filter(ctx); err != nil { return } tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return } values := addPageQuery(url.Values{}, in.Page, in.PerPage) if in.GroupId > 0 { userIds, groupErr := imUserPolicyGroupUserIds(ctx, tenant.Id, in.GroupId) if groupErr != nil { return nil, 0, groupErr } if len(userIds) == 0 { return []*sysin.ImUserListModel{}, 0, nil } values.Set("user_ids", strings.Join(userIds, ",")) } if in.UserId != "" { values.Set("account_no", in.UserId) } if in.Nickname != "" { values.Set("nickname", in.Nickname) } if in.AuthName != "" { values.Set("auth_name", in.AuthName) } if in.Keyword != "" { values.Set("keyword", in.Keyword) } if in.State != nil { values.Set("state", intToString(*in.State)) } addTimeRangeQuery(values, "created_at", in.CreatedAt) addTimeRangeQuery(values, "last_seen", in.LastSeen) list, totalCount, err = imAdminQueryList[sysin.ImUserListModel](ctx, tenant, "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users", values, map[string]string{ "public": "profileJson", "trusted": "trustedJson", "access": "accessJson", }, ) if err != nil { return nil, 0, err } if err = fillImUserPolicyGroups(ctx, tenant.Id, list); err != nil { return nil, 0, err } if err = fillImUserRegisterTypes(ctx, tenant.Id, list); err != nil { return nil, 0, err } return list, totalCount, nil } func fillImUserRegisterTypes(ctx context.Context, tenantId int64, list []*sysin.ImUserListModel) error { if len(list) == 0 { return nil } ids := make([]int64, 0, len(list)) byID := make(map[int64]*sysin.ImUserListModel, len(list)) for _, item := range list { id, err := strconv.ParseInt(item.UserId, 10, 64) if err == nil { ids = append(ids, id) byID[id] = item } if item.RegisterType == "" { item.RegisterType = consts.ImUserRegisterTypeAdmin } } if len(ids) == 0 { return nil } rows, err := imtenant.RegistryDB().Model(imAdminOperationAuditTable).Safe().Ctx(ctx). Fields("target_id").Where("tenant_id", tenantId).Where("target_type", "user"). Where("action", "im_user_create").Where("result", "success").WhereIn("target_id", ids).All() if err != nil { return gerror.Wrap(err, "获取用户注册方式失败") } for _, row := range rows { if item := byID[row["target_id"].Int64()]; item != nil { item.RegisterType = consts.ImUserRegisterTypeAdmin } } return nil } func validateImUserPolicyGroup(ctx context.Context, tenantId, groupId int64) error { c := dao.ImUserPolicyGroups.Columns() count, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroups.Table()).Safe().Ctx(ctx). Where(c.Id, groupId).Where(c.TenantId, tenantId).Where(c.Status, consts.StatusEnabled).WhereNull(c.DeletedAt).Count() if err != nil { return gerror.Wrap(err, "检查用户分组失败") } if count == 0 { return gerror.New("用户分组不存在或已停用") } return nil } func imUserPolicyGroupUserIds(ctx context.Context, tenantId, groupId int64) ([]string, error) { if err := validateImUserPolicyGroup(ctx, tenantId, groupId); err != nil { return nil, err } c := dao.ImUserPolicyGroupMembers.Columns() rows, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx). Fields(c.UserId).Where(c.TenantId, tenantId).Where(c.GroupId, groupId).Where(c.Status, consts.StatusEnabled).WhereNull(c.DeletedAt).All() if err != nil { return nil, gerror.Wrap(err, "获取用户分组成员失败") } ids := make([]string, 0, len(rows)) for _, row := range rows { ids = append(ids, strconv.FormatInt(row[c.UserId].Int64(), 10)) } return ids, nil } func fillImUserPolicyGroups(ctx context.Context, tenantId int64, list []*sysin.ImUserListModel) error { if len(list) == 0 { return nil } ids := make([]int64, 0, len(list)) byId := make(map[int64]*sysin.ImUserListModel, len(list)) for _, item := range list { id, err := strconv.ParseInt(item.UserId, 10, 64) if err == nil { ids = append(ids, id) byId[id] = item } } if len(ids) == 0 { return nil } mc := dao.ImUserPolicyGroupMembers.Columns() gc := dao.ImUserPolicyGroups.Columns() rows, err := imtenant.RegistryDB().Model(dao.ImUserPolicyGroupMembers.Table()+" m").Safe().Ctx(ctx). LeftJoin(dao.ImUserPolicyGroups.Table()+" g", "g."+gc.Id+"=m."+mc.GroupId). Fields("m."+mc.UserId, "g."+gc.Id+" policy_group_id", "g."+gc.GroupName+" policy_group_name"). Where("m."+mc.TenantId, tenantId).WhereIn("m."+mc.UserId, ids). Where("m."+mc.Status, consts.StatusEnabled).Where("g."+gc.Status, consts.StatusEnabled). WhereNull("m." + mc.DeletedAt).WhereNull("g." + gc.DeletedAt).OrderDesc("g." + gc.Priority).All() if err != nil { return gerror.Wrap(err, "获取用户分组信息失败") } for _, row := range rows { item := byId[row[mc.UserId].Int64()] if item != nil && item.PolicyGroupId == 0 { item.PolicyGroupId = row["policy_group_id"].Int64() item.PolicyGroupName = row["policy_group_name"].String() } } return nil } func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model { return db.Model("im_users u").Safe().Ctx(ctx). LeftJoin("im_user_profiles up", "up.user_id = u.id"). LeftJoin("im_user_security us", "us.user_id = u.id"). Fields( "CAST(u.id AS CHAR) user_id", "u.username", "COALESCE(u.phone, '') phone", "u.state", "u.lastseen last_seen", "u.useragent user_agent", "u.createdat created_at", "u.updatedat updated_at", "up.nickname", "up.public_account", "up.gender", "up.region", "COALESCE(up.friend_add_enabled, 1) friend_add_enabled", "us.auth_version", "us.password_updated_at", ) } func (s *sSysImUser) applyListWhere(mod *gdb.Model, in *sysin.ImUserListInp) *gdb.Model { if userId, _ := sysin.ParseOptionalInt64(in.UserId); userId > 0 { mod = mod.Where("u.id", userId) } if in.Nickname != "" { mod = mod.WhereLike("up.nickname", "%"+in.Nickname+"%") } if in.AuthName != "" { mod = mod.Where("u.username = ? OR u.phone = ? OR up.email = ?", in.AuthName, in.AuthName, in.AuthName) } if in.State != nil { mod = mod.Where("u.state", *in.State) } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween("u.createdat", in.CreatedAt[0], in.CreatedAt[1]) } if len(in.LastSeen) == 2 { mod = mod.WhereBetween("u.lastseen", in.LastSeen[0], in.LastSeen[1]) } return mod } func (s *sSysImUser) fillAuthAndCredentials(ctx context.Context, db gdb.DB, list []*sysin.ImUserListModel) (err error) { ids := make([]int64, 0, len(list)) userMap := make(map[int64]*sysin.ImUserListModel, len(list)) for _, item := range list { userId, parseErr := sysin.ParseOptionalInt64(item.UserId) if parseErr != nil { return parseErr } if userId <= 0 { continue } ids = append(ids, userId) userMap[userId] = item } if len(ids) == 0 { return nil } credentialRows, err := db.Model("im_credentials").Safe().Ctx(ctx). Fields("userid", "method", "value", "done"). WhereIn("userid", ids). WhereNull("deletedat"). OrderAsc("method").OrderAsc("value"). All() if err != nil { return gerror.Wrap(err, "获取IM用户凭证失败,请稍后重试!") } credentialMap := make(map[int64][]string) for _, row := range credentialRows { userId := row["userid"].Int64() method := row["method"].String() value := row["value"].String() done := row["done"].Bool() if !done { value += "(未验证)" } credentialMap[userId] = append(credentialMap[userId], method+":"+value) } for userId, item := range userMap { item.AuthNames = item.Username if item.Phone != "" && item.Phone != item.Username { item.AuthNames += ", " + item.Phone } item.Credentials = strings.Join(credentialMap[userId], ", ") } return } // View 获取IM用户指定信息 func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error) { if err = in.Filter(ctx); err != nil { return } tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, nil } return imAdminQueryOne[sysin.ImUserViewModel](ctx, tenant, "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users/"+url.PathEscape(in.UserId), map[string]string{ "public": "profileJson", "trusted": "trustedJson", "access": "accessJson", }, ) } func (s *sSysImUser) viewFields() []interface{} { return []interface{}{ "CAST(u.id AS CHAR) user_id", "u.username", "COALESCE(u.phone, '') phone", "u.state", "u.lastseen last_seen", "u.useragent user_agent", "u.createdat created_at", "u.updatedat updated_at", "up.nickname", "up.public_account", "up.gender", "up.region", "up.signature", "up.avatar_file_id", "CAST(up.birth_date AS CHAR) birth_date", "up.version profile_version", "us.auth_version", "us.password_updated_at", } } func (s *sSysImUser) ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (res *sysin.ImUserCommandModel, err error) { if err = in.Filter(ctx); err != nil { return nil, err } userId, err := sysin.ParseOptionalInt64(in.UserId) if err != nil { return nil, err } res, err = s.syncPasswordReset(ctx, in, userId) result := "success" if err != nil { result = "failed" } tenantId := in.TenantId if res != nil && res.TenantID > 0 { tenantId = res.TenantID } _ = writeImAdminAudit(ctx, tenantId, "user_password_reset", "user", userId, result, nil) return res, err } func (s *sSysImUser) RevokeSessions(ctx context.Context, in *sysin.ImUserRevokeSessionsInp) (err error) { if err = in.Filter(ctx); err != nil { return } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return err } userId, err := sysin.ParseOptionalInt64(in.UserId) if err != nil { return err } db, _, err := imtenant.DB(ctx, scope.TenantId) if err != nil { return err } err = revokeIMUserSessions(ctx, db, scope, userId, "security.sessions_revoked") result := "success" if err != nil { result = "failed" } _ = writeImAdminAudit(ctx, scope.TenantId, "user_sessions_revoke", "user", userId, result, nil) return err } func (s *sSysImUser) Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error) { if err = in.Filter(ctx); err != nil { return } if in.State != consts.ImUserStateOk && in.State != consts.ImUserStateSuspended { return gerror.New("账号状态只能设置为正常或已停用") } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return err } userId, err := sysin.ParseOptionalInt64(in.UserId) if err != nil { return err } db, _, err := imtenant.DB(ctx, scope.TenantId) if err != nil { return err } err = updateIMUserStatus(ctx, db, scope, userId, in.State) result := "success" if err != nil { result = "failed" } _ = writeImAdminAudit(ctx, scope.TenantId, "user_status_update", "user", userId, result, map[string]any{"state": in.State}) return err } func (s *sSysImUser) AdjustGroup(ctx context.Context, in *sysin.ImUserAdjustGroupInp) error { if err := in.Filter(ctx); err != nil { return err } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return err } if err = validateImUserPolicyGroup(ctx, scope.TenantId, in.GroupId); err != nil { return err } db := imtenant.RegistryDB() c := dao.ImUserPolicyGroupMembers.Columns() now := gtime.Now() operatorID := contexts.GetUserId(ctx) err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { _, updateErr := tx.Model(dao.ImUserPolicyGroupMembers.Table()).Safe().Ctx(ctx). Where(c.TenantId, scope.TenantId).Where(c.UserId, in.UserId).WhereNull(c.DeletedAt). Data(g.Map{c.Status: consts.StatusDisable, c.DeletedAt: now, c.DeletedBy: operatorID, c.UpdatedAt: now, c.UpdatedBy: operatorID}).Update() if updateErr != nil { return updateErr } _, insertErr := tx.Exec(`INSERT INTO `+dao.ImUserPolicyGroupMembers.Table()+` (tenant_id,group_id,user_id,status,valid_from,remark,created_at,created_by,updated_at,updated_by,deleted_at,deleted_by) VALUES(?,?,?,1,?,'后台调整分组',?,?,?, ?,NULL,NULL) ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id),status=1,valid_from=VALUES(valid_from),valid_until=NULL, remark=VALUES(remark),updated_at=VALUES(updated_at),updated_by=VALUES(updated_by),deleted_at=NULL,deleted_by=NULL`, scope.TenantId, in.GroupId, in.UserId, now, now, operatorID, now, operatorID) return insertErr }) if err != nil { return gerror.Wrap(err, "调整用户分组失败") } _ = writeImAdminAudit(ctx, scope.TenantId, "im_user_adjust_group", "user", in.UserId, "success", g.Map{"group_id": in.GroupId}) return nil } func (s *sSysImUser) FriendAdd(ctx context.Context, in *sysin.ImUserFriendAddInp) error { if err := in.Filter(ctx); err != nil { return err } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return err } db, _, err := imtenant.DB(ctx, scope.TenantId) if err != nil { return err } now := gtime.Now() result, err := db.Model("im_user_profiles").Safe().Ctx(ctx). Where("user_id", in.UserId). Data(g.Map{ "friend_add_enabled": in.Enabled, "friend_add_disabled_reason": in.Reason, "friend_add_policy_version": gdb.Raw("friend_add_policy_version+1"), "updated_at": now, }).Update() if err != nil { return gerror.Wrap(err, "设置用户加好友开关失败") } affected, _ := result.RowsAffected() if affected == 0 { profileID, idErr := imtenant.NextSnowflakeID(ctx) if idErr != nil { return gerror.Wrap(idErr, "生成用户资料ID失败") } _, err = db.Model("im_user_profiles").Safe().Ctx(ctx).Data(g.Map{ "id": profileID, "user_id": in.UserId, "public_account": fmt.Sprintf("usr%d", in.UserId), "friend_add_enabled": in.Enabled, "friend_add_disabled_reason": in.Reason, "friend_add_policy_version": 1, "created_at": now, "updated_at": now, }).Insert() if err != nil { return gerror.Wrap(err, "补全用户资料并设置加好友开关失败") } } _ = writeImAdminAudit(ctx, scope.TenantId, "im_user_friend_add_update", "user", in.UserId, "success", g.Map{"enabled": in.Enabled, "reason": in.Reason}) return nil } func (s *sSysImUser) IPWhitelist(ctx context.Context, in *sysin.ImUserIPWhitelistInp) (*sysin.ImUserIPWhitelistModel, error) { if err := in.Filter(ctx); err != nil { return nil, err } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return nil, err } db, _, err := imtenant.DB(ctx, scope.TenantId) if err != nil { return nil, err } rows, err := db.Model("im_user_ip_whitelists").Safe().Ctx(ctx). Fields("ip_cidr").Where("user_id", in.UserId). Where("status", consts.StatusEnabled).WhereNull("deleted_at").OrderAsc("id").All() if err != nil { return nil, gerror.Wrap(err, "获取用户IP白名单失败") } result := &sysin.ImUserIPWhitelistModel{IPList: make([]string, 0, len(rows))} for _, row := range rows { result.IPList = append(result.IPList, row["ip_cidr"].String()) } return result, nil } func (s *sSysImUser) IPWhitelistSave(ctx context.Context, in *sysin.ImUserIPWhitelistInp) error { if err := in.Filter(ctx); err != nil { return err } scope, err := resolveImAdminScope(ctx, in.TenantId) if err != nil { return err } unique := make([]string, 0, len(in.IPList)) seen := make(map[string]struct{}, len(in.IPList)) for _, raw := range in.IPList { value := strings.TrimSpace(raw) if value == "" { continue } if net.ParseIP(value) == nil { if _, _, parseErr := net.ParseCIDR(value); parseErr != nil { return gerror.Newf("IP或CIDR格式不合法:%s", value) } } if _, ok := seen[value]; !ok { seen[value] = struct{}{} unique = append(unique, value) } } db, _, err := imtenant.DB(ctx, scope.TenantId) if err != nil { return err } now := gtime.Now() operatorID := contexts.GetUserId(ctx) err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, updateErr := tx.Model("im_user_ip_whitelists").Safe().Ctx(ctx). Where("user_id", in.UserId).WhereNull("deleted_at"). Data(g.Map{"status": consts.StatusDisable, "deleted_at": now, "deleted_by": operatorID, "updated_at": now, "updated_by": operatorID}).Update(); updateErr != nil { return updateErr } for _, value := range unique { if _, insertErr := tx.Exec(`INSERT INTO im_user_ip_whitelists (user_id,ip_cidr,status,created_at,created_by,updated_at,updated_by,deleted_at,deleted_by) VALUES(?,?,1,?,?,?,?,NULL,NULL) ON DUPLICATE KEY UPDATE status=1,updated_at=VALUES(updated_at),updated_by=VALUES(updated_by),deleted_at=NULL,deleted_by=NULL`, in.UserId, value, now, operatorID, now, operatorID); insertErr != nil { return insertErr } } return nil }) if err != nil { return gerror.Wrap(err, "保存用户IP白名单失败") } _ = writeImAdminAudit(ctx, scope.TenantId, "im_user_ip_whitelist_save", "user", in.UserId, "success", g.Map{"ip_list": unique}) return nil } func (s *sSysImUser) Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, err error) { if err = in.Filter(ctx); err != nil { return } tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return } return imAdminQueryList[sysin.ImUserDeviceListModel](ctx, tenant, "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users/"+url.PathEscape(in.UserId)+"/devices", addPageQuery(url.Values{}, in.Page, in.PerPage), nil, ) } func (s *sSysImUser) Ban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserBan) } func (s *sSysImUser) Unban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnban) } func (s *sSysImUser) Freeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserFreeze) } func (s *sSysImUser) Unfreeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnfreeze) } func (s *sSysImUser) DeviceKick(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { if err = in.FilterDevice(ctx); err != nil { return nil, err } return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserDeviceKick) } func (s *sSysImUser) LogoutAll(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserLogoutAll) } func (s *sSysImUser) AccountCancel(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserCancel) } func (s *sSysImUser) AccountDelete(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) { in.Hard = true return s.syncGovernanceCommand(ctx, in, consts.ImAdminCommandUserDelete) } func (s *sSysImUser) syncGovernanceCommand(ctx context.Context, in *sysin.ImUserGovernanceInp, eventType string) (*sysin.ImUserCommandModel, error) { if err := in.Filter(ctx); err != nil { return nil, err } tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } payload := map[string]any{ "event_type": eventType, "actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10), "reason": in.Reason, } if in.DeviceId != "" { payload["device_id"] = in.DeviceId } if eventType == consts.ImAdminCommandUserCancel || eventType == consts.ImAdminCommandUserDelete { payload["hard"] = in.Hard } res, err := s.syncUserAction(ctx, tenant, in.UserId, eventType, payload) if err != nil { return nil, err } return res, nil } func (s *sSysImUser) syncPasswordReset(ctx context.Context, in *sysin.ImUserPasswordResetInp, userId int64) (*sysin.ImUserCommandModel, error) { tenant, err := resolveImAdminTenant(ctx, in.TenantId) if err != nil { return nil, err } passwordHash, err := bcrypt.GenerateFromPassword([]byte(in.NewPassword), bcrypt.DefaultCost) if err != nil { return nil, gerror.Wrap(err, "生成密码摘要失败") } payload := map[string]any{ "event_type": consts.ImAdminCommandUserPasswordReset, "actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10), "password_hash": base64.StdEncoding.EncodeToString(passwordHash), } return s.syncUserAction(ctx, tenant, userId, consts.ImAdminCommandUserPasswordReset, payload) } func (s *sSysImUser) syncUserAction(ctx context.Context, tenant *entity.ImTenant, userId int64, eventType string, payload map[string]any) (*sysin.ImUserCommandModel, error) { var out struct { UserId int64 `json:"user_id"` EventType string `json:"event_type"` Status string `json:"status"` Result map[string]any `json:"result"` } path := "/internal/admin-query/tenants/" + url.PathEscape(tenant.Code) + "/users/" + strconv.FormatInt(userId, 10) + "/action" err := imadmin.DefaultClient(ctx).InternalPost(ctx, path, payload, &out) if err != nil { return nil, err } status := out.Status if status == "" { status = consts.ImAdminCommandStatusSucceeded } return &sysin.ImUserCommandModel{ Status: status, TenantID: tenant.Id, TenantCode: tenant.Code, EventType: eventType, }, nil } func updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, state int) error { securityId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全记录ID失败") } eventId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全事件ID失败") } now := time.Now().UTC().Round(time.Millisecond) requestId := scope.RequestId if requestId == "" { requestId = gctx.CtxId(ctx) } if requestId == "" { requestId = guid.S() } eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("user-status:%d:%d:%d:%s", scope.TenantId, userId, state, requestId))) eventKey := hex.EncodeToString(eventKeyBytes[:]) return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { current, txErr := tx.GetValue("SELECT state FROM im_users WHERE id=? LIMIT 1", userId) if txErr != nil { return txErr } if current.IsNil() { return gerror.New("IM用户不存在") } if current.Int() == consts.ImUserStateDeleted { return gerror.New("已删除账号不能变更状态") } if current.Int() == state { return nil } if _, txErr = tx.Exec("UPDATE im_users SET state=?, updatedat=? WHERE id=?", state, now, userId); txErr != nil { return txErr } if _, txErr = tx.Exec(`INSERT INTO im_user_security (id,user_id,auth_version,created_at,updated_at) VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`, securityId, userId, 2, now, now); txErr != nil { return txErr } version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId) if txErr != nil { return txErr } payload, _ := json.Marshal(map[string]any{ "user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "state": state, "auth_version": version.Int(), }) _, txErr = tx.Exec(`INSERT INTO im_business_events_outbox (id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at) VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?) ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user", fmt.Sprintf("%d", userId), 0, "security.user_status_changed", string(payload), now, now) return txErr }) } func revokeIMUserSessions(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, eventType string) error { securityId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全记录ID失败") } eventId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全事件ID失败") } now := time.Now().UTC().Round(time.Millisecond) requestId := scope.RequestId if requestId == "" { requestId = gctx.CtxId(ctx) } if requestId == "" { requestId = guid.S() } eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%d:%s", eventType, scope.TenantId, userId, requestId))) eventKey := hex.EncodeToString(eventKeyBytes[:]) return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId) if txErr != nil { return txErr } if !exists.Bool() { return gerror.New("IM用户不存在") } if _, txErr = tx.Exec(`INSERT INTO im_user_security (id,user_id,auth_version,created_at,updated_at) VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`, securityId, userId, 2, now, now); txErr != nil { return txErr } version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId) if txErr != nil { return txErr } payload, _ := json.Marshal(map[string]any{ "user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "auth_version": version.Int(), }) _, txErr = tx.Exec(`INSERT INTO im_business_events_outbox (id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at) VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?) ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user", fmt.Sprintf("%d", userId), 0, eventType, string(payload), now, now) return txErr }) } func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, newPassword string) error { passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) if err != nil { return gerror.Wrap(err, "生成密码摘要失败") } securityId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全记录ID失败") } eventId, err := imtenant.NextSnowflakeID(ctx) if err != nil { return gerror.Wrap(err, "生成安全事件ID失败") } now := time.Now().UTC().Round(time.Millisecond) requestId := scope.RequestId if requestId == "" { requestId = gctx.CtxId(ctx) } if requestId == "" { requestId = guid.S() } eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("password-reset:%d:%d:%s", scope.TenantId, userId, requestId))) eventKey := hex.EncodeToString(eventKeyBytes[:]) payload, _ := json.Marshal(map[string]any{ "user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, }) return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId) if txErr != nil { return txErr } if !exists.Bool() { return gerror.New("IM用户不存在") } result, txErr := tx.Exec("UPDATE im_auth SET secret=? WHERE userid=?", passwordHash, userId) if txErr != nil { return txErr } if affected, _ := result.RowsAffected(); affected == 0 { return gerror.New("用户认证记录不存在") } if _, txErr = tx.Exec(`INSERT INTO im_user_security (id,user_id,auth_version,password_updated_at,created_at,updated_at) VALUES(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE auth_version=auth_version+1, password_updated_at=VALUES(password_updated_at),updated_at=VALUES(updated_at)`, securityId, userId, 2, now, now, now); txErr != nil { return txErr } version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId) if txErr != nil { return txErr } var eventPayload map[string]any _ = json.Unmarshal(payload, &eventPayload) eventPayload["auth_version"] = version.Int() payload, _ = json.Marshal(eventPayload) _, txErr = tx.Exec(`INSERT INTO im_business_events_outbox (id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at) VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?) ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user", fmt.Sprintf("%d", userId), 0, "security.password_reset", string(payload), now, now) return txErr }) }