feat: add im kafka admin integration
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
// 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"
|
||||
"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"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sSysImDeadLetter struct{}
|
||||
|
||||
func NewSysImDeadLetter() *sSysImDeadLetter {
|
||||
return &sSysImDeadLetter{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImDeadLetter(NewSysImDeadLetter())
|
||||
}
|
||||
|
||||
// List 获取IM Kafka死信列表
|
||||
func (s *sSysImDeadLetter) List(ctx context.Context, in *sysin.ImDeadLetterListInp) (res *sysin.ImDeadLetterPageModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("limit", strconv.Itoa(in.Limit))
|
||||
if in.Status != "" {
|
||||
values.Set("status", in.Status)
|
||||
}
|
||||
if in.BizType != "" {
|
||||
values.Set("biz_type", in.BizType)
|
||||
}
|
||||
if in.BizId != "" {
|
||||
values.Set("biz_id", in.BizId)
|
||||
}
|
||||
if in.EventType != "" {
|
||||
values.Set("event_type", in.EventType)
|
||||
}
|
||||
if in.Cursor != "" {
|
||||
values.Set("cursor", in.Cursor)
|
||||
}
|
||||
var raw sysin.ImDeadLetterPageRaw
|
||||
path := fmt.Sprintf("/internal/admin-query/tenants/%s/kafka/dead-letters?%s", url.PathEscape(tenant.Code), values.Encode())
|
||||
if err = imadmin.DefaultClient(ctx).InternalGet(ctx, path, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &sysin.ImDeadLetterPageModel{
|
||||
List: make([]*sysin.ImDeadLetterListModel, 0, len(raw.Items)),
|
||||
NextCursor: raw.NextCursor,
|
||||
HasMore: raw.HasMore,
|
||||
}
|
||||
for _, item := range raw.Items {
|
||||
res.List = append(res.List, convertDeadLetterModel(item))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// View 获取IM Kafka死信详情
|
||||
func (s *sSysImDeadLetter) View(ctx context.Context, in *sysin.ImDeadLetterViewInp) (res *sysin.ImDeadLetterViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var raw sysin.ImDeadLetterViewRaw
|
||||
path := fmt.Sprintf("/internal/admin-query/tenants/%s/kafka/dead-letters/%s", url.PathEscape(tenant.Code), url.PathEscape(in.DeadId))
|
||||
if err = imadmin.DefaultClient(ctx).InternalGet(ctx, path, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = &sysin.ImDeadLetterViewModel{
|
||||
ImDeadLetterListModel: *convertDeadLetterModel(&raw.ImDeadLetterListModelRaw),
|
||||
EnvelopeRaw: raw.EnvelopeRaw,
|
||||
}
|
||||
if raw.Envelope != nil {
|
||||
b, _ := json.MarshalIndent(raw.Envelope, "", " ")
|
||||
res.Envelope = string(b)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Replay 重放IM Kafka死信
|
||||
func (s *sSysImDeadLetter) Replay(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
return s.publishActionCommand(ctx, in, consts.ImAdminCommandKafkaDeadReplay)
|
||||
}
|
||||
|
||||
// Ignore 忽略IM Kafka死信
|
||||
func (s *sSysImDeadLetter) Ignore(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
return s.publishActionCommand(ctx, in, consts.ImAdminCommandKafkaDeadIgnore)
|
||||
}
|
||||
|
||||
// Resolve 标记IM Kafka死信已处理
|
||||
func (s *sSysImDeadLetter) Resolve(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
return s.publishActionCommand(ctx, in, consts.ImAdminCommandKafkaDeadResolve)
|
||||
}
|
||||
|
||||
func (s *sSysImDeadLetter) publishActionCommand(ctx context.Context, in *sysin.ImDeadLetterActionInp, eventType string) (*sysin.ImGroupCommandModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant, err := s.getTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actorName := ""
|
||||
if user != nil {
|
||||
actorName = user.RealName
|
||||
}
|
||||
command, err := imkafka.PublishAdminCommand(ctx, imkafka.AdminCommandInput{
|
||||
TenantID: tenant.Id,
|
||||
TenantCode: tenant.Code,
|
||||
EventType: eventType,
|
||||
BizType: "kafka_dead_letter",
|
||||
BizID: in.DeadId,
|
||||
Actor: &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
DisplayName: actorName,
|
||||
},
|
||||
Payload: map[string]any{
|
||||
"command_id": "",
|
||||
"actor_user_id": strconv.FormatInt(in.ActorUserId, 10),
|
||||
"dead_id": in.DeadId,
|
||||
"note": in.Note,
|
||||
},
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImGroupCommandModel{
|
||||
CommandID: command.CommandID,
|
||||
EventID: command.EventID,
|
||||
Status: command.Status,
|
||||
TenantID: command.TenantID,
|
||||
TenantCode: command.TenantCode,
|
||||
EventType: command.EventType,
|
||||
IdempotencyKey: command.IdempotencyKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImDeadLetter) getTenant(ctx context.Context, tenantId int64) (tenant *entity.ImTenant, err error) {
|
||||
if tenantId <= 0 {
|
||||
return nil, gerror.New("请选择IM租户")
|
||||
}
|
||||
if err = imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
return nil, gerror.New("IM租户不存在")
|
||||
}
|
||||
if tenant.State != consts.ImTenantStateActive {
|
||||
return nil, gerror.New("IM租户未启用")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func convertDeadLetterModel(in *sysin.ImDeadLetterListModelRaw) *sysin.ImDeadLetterListModel {
|
||||
if in == nil {
|
||||
return &sysin.ImDeadLetterListModel{}
|
||||
}
|
||||
return &sysin.ImDeadLetterListModel{
|
||||
Id: in.Id,
|
||||
DeadId: in.DeadId,
|
||||
EventId: in.EventId,
|
||||
ParentEventId: in.ParentEventId,
|
||||
SourceTopic: in.SourceTopic,
|
||||
ConsumerGroup: in.ConsumerGroup,
|
||||
EventType: in.EventType,
|
||||
BizType: in.BizType,
|
||||
BizId: in.BizId,
|
||||
RetryCount: in.RetryCount,
|
||||
LastReason: in.LastReason,
|
||||
LastError: in.LastError,
|
||||
Status: in.Status,
|
||||
ReplayCount: in.ReplayCount,
|
||||
CreatedAt: in.CreatedAt,
|
||||
UpdatedAt: in.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,14 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@@ -454,3 +459,220 @@ func (s *sSysImGroup) AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLo
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProfileUpdate 通过Kafka修改IM群资料
|
||||
func (s *sSysImGroup) ProfileUpdate(ctx context.Context, in *sysin.ImGroupProfileUpdateInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"profile": map[string]any{},
|
||||
"version": in.Version,
|
||||
}
|
||||
profile := payload["profile"].(map[string]any)
|
||||
if in.Name != "" {
|
||||
profile["name"] = in.Name
|
||||
}
|
||||
if in.AvatarFileId != "" {
|
||||
profile["avatar_file_id"] = in.AvatarFileId
|
||||
}
|
||||
if in.Description != "" {
|
||||
profile["description"] = in.Description
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupProfileUpdate, in.Topic, int64(in.Version), payload)
|
||||
}
|
||||
|
||||
// GovernanceUpdate 通过Kafka修改IM群治理配置
|
||||
func (s *sSysImGroup) GovernanceUpdate(ctx context.Context, in *sysin.ImGroupGovernanceUpdateInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"governance": map[string]any{},
|
||||
"version": in.Version,
|
||||
}
|
||||
governance := payload["governance"].(map[string]any)
|
||||
if in.JoinApproval != nil {
|
||||
governance["join_approval"] = *in.JoinApproval
|
||||
}
|
||||
if in.MuteAll != nil {
|
||||
governance["mute_all"] = *in.MuteAll
|
||||
}
|
||||
if in.MemberUploadFiles != nil {
|
||||
governance["member_upload_files"] = *in.MemberUploadFiles
|
||||
}
|
||||
if in.MemberPrivateChat != nil {
|
||||
governance["member_private_chat"] = *in.MemberPrivateChat
|
||||
}
|
||||
if in.MemberInvite != nil {
|
||||
governance["member_invite"] = *in.MemberInvite
|
||||
}
|
||||
if len(governance) == 0 {
|
||||
return nil, gerror.New("请至少选择一个治理配置")
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupGovernUpdate, in.Topic, int64(in.Version), payload)
|
||||
}
|
||||
|
||||
// AdminSet 通过Kafka设置IM群管理员
|
||||
func (s *sSysImGroup) AdminSet(ctx context.Context, in *sysin.ImGroupAdminSetInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || len(in.UserIds) == 0 {
|
||||
return nil, gerror.New("操作IM用户ID和用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"user_ids": imUserIds(in.UserIds),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupAdminSet, in.Topic, 0, payload)
|
||||
}
|
||||
|
||||
// AdminRemove 通过Kafka删除IM群管理员
|
||||
func (s *sSysImGroup) AdminRemove(ctx context.Context, in *sysin.ImGroupAdminRemoveInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || in.UserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID和用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"user_id": imUserId(in.UserId),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupAdminRemove, in.Topic, 0, payload)
|
||||
}
|
||||
|
||||
// MemberMute 通过Kafka禁言IM群成员
|
||||
func (s *sSysImGroup) MemberMute(ctx context.Context, in *sysin.ImGroupMemberMuteInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || in.UserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID和用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"user_id": imUserId(in.UserId),
|
||||
"reason": in.Reason,
|
||||
}
|
||||
if in.ExpiresAt != nil {
|
||||
payload["expires_at"] = in.ExpiresAt.Time.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupMemberMute, in.Topic, 0, payload)
|
||||
}
|
||||
|
||||
// MemberUnmute 通过Kafka解除IM群成员禁言
|
||||
func (s *sSysImGroup) MemberUnmute(ctx context.Context, in *sysin.ImGroupMemberUnmuteInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || in.UserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID和用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"user_id": imUserId(in.UserId),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupMemberUnmute, in.Topic, 0, payload)
|
||||
}
|
||||
|
||||
// MemberRemove 通过Kafka移出IM群成员
|
||||
func (s *sSysImGroup) MemberRemove(ctx context.Context, in *sysin.ImGroupMemberRemoveInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || in.UserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID和用户ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"user_id": imUserId(in.UserId),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupMemberRemove, in.Topic, 0, payload)
|
||||
}
|
||||
|
||||
// OwnerTransfer 通过Kafka转让IM群主
|
||||
func (s *sSysImGroup) OwnerTransfer(ctx context.Context, in *sysin.ImGroupOwnerTransferInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if in.ActorUserId <= 0 || in.TargetUserId <= 0 {
|
||||
return nil, gerror.New("操作IM用户ID和新群主ID不能为空")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": imUserId(in.ActorUserId),
|
||||
"topic": in.Topic,
|
||||
"target_user_id": imUserId(in.TargetUserId),
|
||||
"version": in.Version,
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, consts.ImAdminCommandGroupOwnerTransfer, in.Topic, int64(in.Version), payload)
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) publishGroupCommand(ctx context.Context, tenantId int64, eventType string, topic string, version int64, payload map[string]any) (*sysin.ImGroupCommandModel, error) {
|
||||
_, tenant, err := imtenant.DB(ctx, tenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actorName := ""
|
||||
if user != nil {
|
||||
actorName = user.RealName
|
||||
}
|
||||
command, err := imkafka.PublishAdminCommand(ctx, imkafka.AdminCommandInput{
|
||||
TenantID: tenant.Id,
|
||||
TenantCode: tenant.Code,
|
||||
EventType: eventType,
|
||||
BizType: "group",
|
||||
BizID: topic,
|
||||
Version: version,
|
||||
Actor: &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
DisplayName: actorName,
|
||||
},
|
||||
Payload: payload,
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImGroupCommandModel{
|
||||
CommandID: command.CommandID,
|
||||
EventID: command.EventID,
|
||||
Status: command.Status,
|
||||
TenantID: command.TenantID,
|
||||
TenantCode: command.TenantCode,
|
||||
EventType: command.EventType,
|
||||
IdempotencyKey: command.IdempotencyKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func imUserId(id int64) string {
|
||||
return strconv.FormatInt(id, 10)
|
||||
}
|
||||
|
||||
func imUserIds(ids []int64) []string {
|
||||
list := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id > 0 {
|
||||
list = append(list, imUserId(id))
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sSysImKafkaAudit struct{}
|
||||
|
||||
func NewSysImKafkaAudit() *sSysImKafkaAudit {
|
||||
return &sSysImKafkaAudit{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImKafkaAudit(NewSysImKafkaAudit())
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaAudit) List(ctx context.Context, in *sysin.ImKafkaAuditListInp) (list []*sysin.ImKafkaAuditListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
mod := g.DB().Model("im_audit_events").Safe().Ctx(ctx).
|
||||
Fields("id", "event_id", "tenant_code", "event_type", "biz_type", "biz_id", "actor_type", "actor_id", "action", "resource", "resource_id", "result", "reason", "occurred_at", "received_at")
|
||||
if in.TenantCode != "" {
|
||||
mod = mod.Where("tenant_code", in.TenantCode)
|
||||
}
|
||||
if in.EventId != "" {
|
||||
mod = mod.WhereLike("event_id", "%"+in.EventId+"%")
|
||||
}
|
||||
if in.EventType != "" {
|
||||
mod = mod.WhereLike("event_type", "%"+in.EventType+"%")
|
||||
}
|
||||
if in.BizType != "" {
|
||||
mod = mod.Where("biz_type", in.BizType)
|
||||
}
|
||||
if in.BizId != "" {
|
||||
mod = mod.WhereLike("biz_id", "%"+in.BizId+"%")
|
||||
}
|
||||
if in.ActorType != "" {
|
||||
mod = mod.Where("actor_type", in.ActorType)
|
||||
}
|
||||
if in.ActorId != "" {
|
||||
mod = mod.WhereLike("actor_id", "%"+in.ActorId+"%")
|
||||
}
|
||||
if in.Action != "" {
|
||||
mod = mod.WhereLike("action", "%"+in.Action+"%")
|
||||
}
|
||||
if in.Resource != "" {
|
||||
mod = mod.Where("resource", in.Resource)
|
||||
}
|
||||
if in.ResourceId != "" {
|
||||
mod = mod.WhereLike("resource_id", "%"+in.ResourceId+"%")
|
||||
}
|
||||
if in.Result != "" {
|
||||
mod = mod.Where("result", in.Result)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("received_at", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM Kafka审计事件列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaAudit) View(ctx context.Context, in *sysin.ImKafkaAuditViewInp) (res *sysin.ImKafkaAuditViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mod := g.DB().Model("im_audit_events").Safe().Ctx(ctx).
|
||||
Fields("id", "event_id", "tenant_code", "event_type", "biz_type", "biz_id", "actor_type", "actor_id", "action", "resource", "resource_id", "result", "reason", "CAST(payload AS CHAR) payload", "CAST(envelope AS CHAR) envelope", "occurred_at", "received_at")
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where("id", in.Id)
|
||||
} else {
|
||||
mod = mod.Where("event_id", in.EventId)
|
||||
}
|
||||
if err = mod.Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM Kafka审计事件详情失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// 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"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sSysImKafkaCommand struct{}
|
||||
|
||||
func NewSysImKafkaCommand() *sSysImKafkaCommand {
|
||||
return &sSysImKafkaCommand{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImKafkaCommand(NewSysImKafkaCommand())
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandListInp) (list []*sysin.ImKafkaCommandListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
mod := g.DB().Model("im_admin_commands").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"command_id",
|
||||
"tenant_id",
|
||||
"tenant_code",
|
||||
"event_id",
|
||||
"event_type",
|
||||
"biz_type",
|
||||
"biz_id",
|
||||
"idempotency_key",
|
||||
"status",
|
||||
"reason",
|
||||
"message",
|
||||
"sent_at",
|
||||
"completed_at",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
if in.TenantId > 0 {
|
||||
mod = mod.Where("tenant_id", in.TenantId)
|
||||
}
|
||||
if in.CommandId != "" {
|
||||
mod = mod.WhereLike("command_id", "%"+in.CommandId+"%")
|
||||
}
|
||||
if in.EventId != "" {
|
||||
mod = mod.WhereLike("event_id", "%"+in.EventId+"%")
|
||||
}
|
||||
if in.EventType != "" {
|
||||
mod = mod.WhereLike("event_type", "%"+in.EventType+"%")
|
||||
}
|
||||
if in.BizType != "" {
|
||||
mod = mod.Where("biz_type", in.BizType)
|
||||
}
|
||||
if in.BizId != "" {
|
||||
mod = mod.WhereLike("biz_id", "%"+in.BizId+"%")
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM后台命令列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaCommand) View(ctx context.Context, in *sysin.ImKafkaCommandViewInp) (res *sysin.ImKafkaCommandViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mod := g.DB().Model("im_admin_commands").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"command_id",
|
||||
"tenant_id",
|
||||
"tenant_code",
|
||||
"event_id",
|
||||
"event_type",
|
||||
"biz_type",
|
||||
"biz_id",
|
||||
"idempotency_key",
|
||||
"status",
|
||||
"reason",
|
||||
"message",
|
||||
"CAST(payload AS CHAR) payload",
|
||||
"CAST(envelope AS CHAR) envelope",
|
||||
"CAST(result AS CHAR) result",
|
||||
"sent_at",
|
||||
"completed_at",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where("id", in.Id)
|
||||
} else {
|
||||
mod = mod.Where("command_id", in.CommandId)
|
||||
}
|
||||
if err = mod.Scan(&res); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM后台命令详情失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaCommand) CacheInvalidate(ctx context.Context, in *sysin.ImKafkaCacheInvalidateInp) (res *sysin.ImKafkaEventModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actor := &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: gconv.String(contexts.GetUserId(ctx)),
|
||||
}
|
||||
if user != nil {
|
||||
actor.DisplayName = user.RealName
|
||||
}
|
||||
payload := map[string]any{
|
||||
"tenant_code": in.TenantCode,
|
||||
"namespace": in.Namespace,
|
||||
"keys": in.Keys,
|
||||
"reason": in.Reason,
|
||||
}
|
||||
eventID, err := imkafka.PublishEvent(ctx, imkafka.EventInput{
|
||||
Topic: consts.QueueImCacheInvalidateTopic,
|
||||
TenantCode: in.TenantCode,
|
||||
EventType: "cache.invalidate",
|
||||
BizType: "cache",
|
||||
BizID: in.Namespace,
|
||||
Actor: actor,
|
||||
Payload: payload,
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImKafkaEventModel{EventId: eventID}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImKafkaCommand) WorkbenchEvent(ctx context.Context, in *sysin.ImKafkaWorkbenchEventInp) (res *sysin.ImKafkaEventModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actor := &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: gconv.String(contexts.GetUserId(ctx)),
|
||||
}
|
||||
if user != nil {
|
||||
actor.DisplayName = user.RealName
|
||||
}
|
||||
payload := in.Payload
|
||||
payload["tenant_code"] = in.TenantCode
|
||||
payload["source"] = "admin"
|
||||
eventID, err := imkafka.PublishEvent(ctx, imkafka.EventInput{
|
||||
Topic: consts.QueueImWorkbenchEventTopic,
|
||||
TenantCode: in.TenantCode,
|
||||
EventType: in.EventType,
|
||||
BizType: "workbench",
|
||||
BizID: in.BizId,
|
||||
Actor: actor,
|
||||
Payload: payload,
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImKafkaEventModel{EventId: eventID}, nil
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,6 +65,24 @@ func (s *sSysImRegistration) UpdateConfig(ctx context.Context, in *sysin.ImRegis
|
||||
"old": old.RegisterSmsEnabled,
|
||||
"new": in.RegisterSmsEnabled,
|
||||
})
|
||||
actorName := ""
|
||||
if user := contexts.GetUser(ctx); user != nil {
|
||||
actorName = user.RealName
|
||||
}
|
||||
if eventErr := imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
||||
EventType: consts.ImConfigEventRegistrationUpdated,
|
||||
BizID: "registration",
|
||||
Group: imRegistrationConfigGroup,
|
||||
Keys: []string{imRegistrationSmsKey},
|
||||
Actor: &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: gconv.String(contexts.GetUserId(ctx)),
|
||||
DisplayName: actorName,
|
||||
},
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
}); eventErr != nil {
|
||||
g.Log().Warningf(ctx, "发送IM注册配置Kafka事件失败 err:%+v", eventErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
@@ -19,6 +21,7 @@ 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/util/gconv"
|
||||
)
|
||||
|
||||
type sSysImTenant struct{}
|
||||
@@ -142,7 +145,7 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err
|
||||
return
|
||||
}
|
||||
|
||||
return imtenant.RegistryDB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
err = imtenant.RegistryDB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -167,6 +170,12 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err
|
||||
}
|
||||
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.publishTenantConfigEvent(ctx, in.Code, in.Id, consts.ImConfigEventTenantUpdated, "tenant")
|
||||
s.publishTenantConfigEvent(ctx, in.Code, in.Id, consts.ImConfigEventTenantLimitsUpdated, "tenant_limits")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
|
||||
@@ -269,5 +278,33 @@ func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp)
|
||||
err = gerror.Wrap(err, "更新IM租户状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
var tenant *entity.ImTenant
|
||||
if scanErr := s.Model(ctx).Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().Code).WherePri(in.Id).Scan(&tenant); scanErr == nil && tenant != nil {
|
||||
s.publishTenantConfigEvent(ctx, tenant.Code, tenant.Id, consts.ImConfigEventTenantStatusChanged, "tenant_status")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) publishTenantConfigEvent(ctx context.Context, tenantCode string, tenantId int64, eventType string, bizId string) {
|
||||
user := contexts.GetUser(ctx)
|
||||
actorName := ""
|
||||
if user != nil {
|
||||
actorName = user.RealName
|
||||
}
|
||||
if err := imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
||||
TenantID: tenantId,
|
||||
TenantCode: tenantCode,
|
||||
EventType: eventType,
|
||||
BizID: bizId,
|
||||
Group: "tenant",
|
||||
Keys: []string{"tenant", "limits", "features"},
|
||||
Actor: &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: gconv.String(contexts.GetUserId(ctx)),
|
||||
DisplayName: actorName,
|
||||
},
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
}); err != nil {
|
||||
g.Log().Warningf(ctx, "发送IM租户配置Kafka事件失败 tenant:%s type:%s err:%+v", tenantCode, eventType, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,13 @@ package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
@@ -187,6 +191,103 @@ func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sy
|
||||
return
|
||||
}
|
||||
|
||||
// Ban 通过Kafka封禁IM用户
|
||||
func (s *sSysImUser) Ban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserBan)
|
||||
}
|
||||
|
||||
// Unban 通过Kafka解封IM用户
|
||||
func (s *sSysImUser) Unban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnban)
|
||||
}
|
||||
|
||||
// Freeze 通过Kafka冻结IM用户
|
||||
func (s *sSysImUser) Freeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserFreeze)
|
||||
}
|
||||
|
||||
// Unfreeze 通过Kafka解冻IM用户
|
||||
func (s *sSysImUser) Unfreeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnfreeze)
|
||||
}
|
||||
|
||||
// DeviceKick 通过Kafka踢出IM用户设备
|
||||
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.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserDeviceKick)
|
||||
}
|
||||
|
||||
// LogoutAll 通过Kafka踢出IM用户全部在线会话
|
||||
func (s *sSysImUser) LogoutAll(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserLogoutAll)
|
||||
}
|
||||
|
||||
// AccountCancel 通过Kafka注销IM用户账号
|
||||
func (s *sSysImUser) AccountCancel(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserCancel)
|
||||
}
|
||||
|
||||
// AccountDelete 通过Kafka删除IM用户账号
|
||||
func (s *sSysImUser) AccountDelete(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
in.Hard = true
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserDelete)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) publishGovernanceCommand(ctx context.Context, in *sysin.ImUserGovernanceInp, eventType string) (*sysin.ImUserCommandModel, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := contexts.GetUser(ctx)
|
||||
actorName := ""
|
||||
if user != nil {
|
||||
actorName = user.RealName
|
||||
}
|
||||
payload := map[string]any{
|
||||
"actor_user_id": strconv.FormatInt(in.ActorUserId, 10),
|
||||
"user_id": strconv.FormatInt(in.UserId, 10),
|
||||
"target_user_id": strconv.FormatInt(in.UserId, 10),
|
||||
"reason": in.Reason,
|
||||
}
|
||||
if in.DeviceId != "" {
|
||||
payload["device_id"] = in.DeviceId
|
||||
}
|
||||
if eventType == consts.ImAdminCommandUserCancel || eventType == consts.ImAdminCommandUserDelete {
|
||||
payload["hard"] = in.Hard
|
||||
}
|
||||
command, err := imkafka.PublishAdminCommand(ctx, imkafka.AdminCommandInput{
|
||||
TenantID: tenant.Id,
|
||||
TenantCode: tenant.Code,
|
||||
EventType: eventType,
|
||||
BizType: "user",
|
||||
BizID: strconv.FormatInt(in.UserId, 10),
|
||||
Actor: &imkafka.Actor{
|
||||
Type: "admin",
|
||||
ID: strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
DisplayName: actorName,
|
||||
},
|
||||
Payload: payload,
|
||||
CreatedBy: contexts.GetUserId(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImUserCommandModel{
|
||||
CommandID: command.CommandID,
|
||||
EventID: command.EventID,
|
||||
Status: command.Status,
|
||||
TenantID: command.TenantID,
|
||||
TenantCode: command.TenantCode,
|
||||
EventType: command.EventType,
|
||||
IdempotencyKey: command.IdempotencyKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImUser) viewFields() []interface{} {
|
||||
return []interface{}{
|
||||
"u.id user_id",
|
||||
|
||||
Reference in New Issue
Block a user