feat: 完善IM用户管理与治理操作

This commit is contained in:
richjohnrambo
2026-09-08 10:54:01 +08:00
parent 5f3f6913b8
commit 7d54bf785a
13 changed files with 1319 additions and 131 deletions
+384 -1
View File
@@ -13,12 +13,17 @@ import (
"encoding/json"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/imadmin"
"hotgo/internal/library/imkafka"
"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"
@@ -26,8 +31,11 @@ import (
"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"
)
@@ -41,6 +49,141 @@ 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 {
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{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.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 {
@@ -51,6 +194,16 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
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("user_id", in.UserId)
}
@@ -60,12 +213,15 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
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)
return imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
list, totalCount, err = imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
"/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users",
values,
map[string]string{
@@ -74,6 +230,116 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
"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 {
@@ -285,6 +551,123 @@ func (s *sSysImUser) Status(ctx context.Context, in *sysin.ImUserStatusInp) (err
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) 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