feat: add im kafka admin integration

This commit is contained in:
richjohnrambo
2026-09-04 11:57:38 +08:00
parent a2abaef3a6
commit f7f8cc0ef0
74 changed files with 6254 additions and 47 deletions
@@ -0,0 +1,59 @@
package imconfiginternal
import "github.com/gogf/gf/v2/frame/g"
type TenantReq struct {
g.Meta `path:"/internal/im-config/tenants/by-code/{code}" method:"get" tags:"IM内部配置" summary:"按编码获取IM租户"`
Code string `json:"code" in:"path" dc:"租户编码"`
}
type TenantByIDReq struct {
g.Meta `path:"/internal/im-config/tenants/by-id/{id}" method:"get" tags:"IM内部配置" summary:"按ID获取IM租户"`
Id int64 `json:"id" in:"path" dc:"租户ID"`
}
type TenantListReq struct {
g.Meta `path:"/internal/im-config/tenants" method:"get" tags:"IM内部配置" summary:"获取IM租户列表"`
State string `json:"state" dc:"状态"`
Limit int `json:"limit" dc:"数量"`
}
type TenantRes struct {
*TenantModel
}
type TenantListRes struct {
Items []*TenantModel `json:"items" dc:"租户列表"`
}
type TenantModel struct {
ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
TenantDesc *string `json:"tenant_desc,omitempty"`
DBName string `json:"db_name"`
DBDSN string `json:"db_dsn"`
State int `json:"state"`
CreatedAt string `json:"created_at"`
CreatedBy int64 `json:"created_by"`
UpdatedAt string `json:"updated_at"`
UpdatedBy int64 `json:"updated_by"`
}
type RegistrationSmsReq struct {
g.Meta `path:"/internal/im-config/registration/sms" method:"get" tags:"IM内部配置" summary:"获取注册短信开关"`
}
type RegistrationSmsRes struct {
Enabled bool `json:"enabled" dc:"是否开启"`
}
type ThirdPartyReq struct {
g.Meta `path:"/internal/im-config/third-party/{group}" method:"get" tags:"IM内部配置" summary:"获取第三方配置"`
Group string `json:"group" in:"path" dc:"配置组"`
Keys string `json:"keys" dc:"配置键,逗号分隔"`
}
type ThirdPartyRes struct {
Values map[string]string `json:"values" dc:"配置值"`
}
@@ -0,0 +1,57 @@
// Package imdeadletter
// @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 imdeadletter
import (
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imDeadLetter/list" method:"get" tags:"IM Kafka死信" summary:"获取IM Kafka死信列表"`
sysin.ImDeadLetterListInp
}
type ListRes struct {
*sysin.ImDeadLetterPageModel
}
type ViewReq struct {
g.Meta `path:"/imDeadLetter/view" method:"get" tags:"IM Kafka死信" summary:"获取IM Kafka死信详情"`
sysin.ImDeadLetterViewInp
}
type ViewRes struct {
*sysin.ImDeadLetterViewModel
}
type ReplayReq struct {
g.Meta `path:"/imDeadLetter/replay" method:"post" tags:"IM Kafka死信" summary:"重放IM Kafka死信"`
sysin.ImDeadLetterActionInp
}
type ReplayRes struct {
*sysin.ImGroupCommandModel
}
type IgnoreReq struct {
g.Meta `path:"/imDeadLetter/ignore" method:"post" tags:"IM Kafka死信" summary:"忽略IM Kafka死信"`
sysin.ImDeadLetterActionInp
}
type IgnoreRes struct {
*sysin.ImGroupCommandModel
}
type ResolveReq struct {
g.Meta `path:"/imDeadLetter/resolve" method:"post" tags:"IM Kafka死信" summary:"标记IM Kafka死信已处理"`
sysin.ImDeadLetterActionInp
}
type ResolveRes struct {
*sysin.ImGroupCommandModel
}
+80
View File
@@ -108,3 +108,83 @@ type AdminLogListRes struct {
form.PageRes
List []*sysin.ImGroupAdminLogListModel `json:"list" dc:"数据列表"`
}
// ProfileUpdateReq 通过Kafka修改IM群资料
type ProfileUpdateReq struct {
g.Meta `path:"/imGroup/profileUpdate" method:"post" tags:"IM群组" summary:"Kafka修改IM群资料"`
sysin.ImGroupProfileUpdateInp
}
type ProfileUpdateRes struct {
*sysin.ImGroupCommandModel
}
// GovernanceUpdateReq 通过Kafka修改IM群治理配置
type GovernanceUpdateReq struct {
g.Meta `path:"/imGroup/governanceUpdate" method:"post" tags:"IM群组" summary:"Kafka修改IM群治理配置"`
sysin.ImGroupGovernanceUpdateInp
}
type GovernanceUpdateRes struct {
*sysin.ImGroupCommandModel
}
// AdminSetReq 通过Kafka设置IM群管理员
type AdminSetReq struct {
g.Meta `path:"/imGroup/adminSet" method:"post" tags:"IM群组" summary:"Kafka设置IM群管理员"`
sysin.ImGroupAdminSetInp
}
type AdminSetRes struct {
*sysin.ImGroupCommandModel
}
// AdminRemoveReq 通过Kafka删除IM群管理员
type AdminRemoveReq struct {
g.Meta `path:"/imGroup/adminRemove" method:"post" tags:"IM群组" summary:"Kafka删除IM群管理员"`
sysin.ImGroupAdminRemoveInp
}
type AdminRemoveRes struct {
*sysin.ImGroupCommandModel
}
// MemberMuteReq 通过Kafka禁言IM群成员
type MemberMuteReq struct {
g.Meta `path:"/imGroup/memberMute" method:"post" tags:"IM群组" summary:"Kafka禁言IM群成员"`
sysin.ImGroupMemberMuteInp
}
type MemberMuteRes struct {
*sysin.ImGroupCommandModel
}
// MemberUnmuteReq 通过Kafka解除IM群成员禁言
type MemberUnmuteReq struct {
g.Meta `path:"/imGroup/memberUnmute" method:"post" tags:"IM群组" summary:"Kafka解除IM群成员禁言"`
sysin.ImGroupMemberUnmuteInp
}
type MemberUnmuteRes struct {
*sysin.ImGroupCommandModel
}
// MemberRemoveReq 通过Kafka移出IM群成员
type MemberRemoveReq struct {
g.Meta `path:"/imGroup/memberRemove" method:"post" tags:"IM群组" summary:"Kafka移出IM群成员"`
sysin.ImGroupMemberRemoveInp
}
type MemberRemoveRes struct {
*sysin.ImGroupCommandModel
}
// OwnerTransferReq 通过Kafka转让IM群主
type OwnerTransferReq struct {
g.Meta `path:"/imGroup/ownerTransfer" method:"post" tags:"IM群组" summary:"Kafka转让IM群主"`
sysin.ImGroupOwnerTransferInp
}
type OwnerTransferRes struct {
*sysin.ImGroupCommandModel
}
@@ -0,0 +1,27 @@
package imkafkaaudit
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imKafkaAudit/list" method:"get" tags:"IM Kafka审计事件" summary:"获取IM Kafka审计事件列表"`
sysin.ImKafkaAuditListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImKafkaAuditListModel `json:"list" dc:"数据列表"`
}
type ViewReq struct {
g.Meta `path:"/imKafkaAudit/view" method:"get" tags:"IM Kafka审计事件" summary:"获取IM Kafka审计事件详情"`
sysin.ImKafkaAuditViewInp
}
type ViewRes struct {
*sysin.ImKafkaAuditViewModel
}
@@ -0,0 +1,50 @@
// Package imkafkacommand
// @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 imkafkacommand
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imKafkaCommand/list" method:"get" tags:"IM后台命令" summary:"获取IM后台命令列表"`
sysin.ImKafkaCommandListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImKafkaCommandListModel `json:"list" dc:"数据列表"`
}
type ViewReq struct {
g.Meta `path:"/imKafkaCommand/view" method:"get" tags:"IM后台命令" summary:"获取IM后台命令详情"`
sysin.ImKafkaCommandViewInp
}
type ViewRes struct {
*sysin.ImKafkaCommandViewModel
}
type CacheInvalidateReq struct {
g.Meta `path:"/imKafkaCommand/cacheInvalidate" method:"post" tags:"IM后台命令" summary:"发送IM缓存失效测试事件"`
sysin.ImKafkaCacheInvalidateInp
}
type CacheInvalidateRes struct {
*sysin.ImKafkaEventModel
}
type WorkbenchEventReq struct {
g.Meta `path:"/imKafkaCommand/workbenchEvent" method:"post" tags:"IM后台命令" summary:"发送IM工作台测试事件"`
sysin.ImKafkaWorkbenchEventInp
}
type WorkbenchEventRes struct {
*sysin.ImKafkaEventModel
}
+72
View File
@@ -32,3 +32,75 @@ type ViewReq struct {
type ViewRes struct {
*sysin.ImUserViewModel
}
type BanReq struct {
g.Meta `path:"/imUser/ban" method:"post" tags:"IM用户管理" summary:"封禁IM用户"`
sysin.ImUserGovernanceInp
}
type BanRes struct {
*sysin.ImUserCommandModel
}
type UnbanReq struct {
g.Meta `path:"/imUser/unban" method:"post" tags:"IM用户管理" summary:"解封IM用户"`
sysin.ImUserGovernanceInp
}
type UnbanRes struct {
*sysin.ImUserCommandModel
}
type FreezeReq struct {
g.Meta `path:"/imUser/freeze" method:"post" tags:"IM用户管理" summary:"冻结IM用户"`
sysin.ImUserGovernanceInp
}
type FreezeRes struct {
*sysin.ImUserCommandModel
}
type UnfreezeReq struct {
g.Meta `path:"/imUser/unfreeze" method:"post" tags:"IM用户管理" summary:"解冻IM用户"`
sysin.ImUserGovernanceInp
}
type UnfreezeRes struct {
*sysin.ImUserCommandModel
}
type DeviceKickReq struct {
g.Meta `path:"/imUser/deviceKick" method:"post" tags:"IM用户管理" summary:"踢出IM用户设备"`
sysin.ImUserGovernanceInp
}
type DeviceKickRes struct {
*sysin.ImUserCommandModel
}
type LogoutAllReq struct {
g.Meta `path:"/imUser/logoutAll" method:"post" tags:"IM用户管理" summary:"踢出IM用户全部在线会话"`
sysin.ImUserGovernanceInp
}
type LogoutAllRes struct {
*sysin.ImUserCommandModel
}
type AccountCancelReq struct {
g.Meta `path:"/imUser/accountCancel" method:"post" tags:"IM用户管理" summary:"注销IM用户账号"`
sysin.ImUserGovernanceInp
}
type AccountCancelRes struct {
*sysin.ImUserCommandModel
}
type AccountDeleteReq struct {
g.Meta `path:"/imUser/accountDelete" method:"post" tags:"IM用户管理" summary:"删除IM用户账号"`
sysin.ImUserGovernanceInp
}
type AccountDeleteRes struct {
*sysin.ImUserCommandModel
}
+3
View File
@@ -35,6 +35,7 @@ require (
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1200
github.com/tencentyun/cos-go-sdk-v5 v0.7.66
github.com/ufilesdk-dev/ufile-gosdk v1.0.6
github.com/xdg-go/scram v1.1.2
github.com/xuri/excelize/v2 v2.9.1
go.opentelemetry.io/otel v1.38.0
golang.org/x/mod v0.26.0
@@ -138,6 +139,8 @@ require (
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
+7
View File
@@ -446,6 +446,12 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ufilesdk-dev/ufile-gosdk v1.0.6 h1:YhTZfA0U5zjKUe6qYrIs72jjxSZryTMbS9wE76fIxMs=
github.com/ufilesdk-dev/ufile-gosdk v1.0.6/go.mod h1:s1LIpscsgVNx0JRNbDElNNQhyNXJiumVpzvPzVNS/XI=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
@@ -618,6 +624,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+3
View File
@@ -45,6 +45,9 @@ var (
}...)
s.Group("/", func(group *ghttp.RouterGroup) {
// 注册内部服务路由
router.Internal(ctx, group)
// 注册后台路由
router.Admin(ctx, group)
+88
View File
@@ -0,0 +1,88 @@
// Package consts
// @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 consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("ImAdminCommandStatusOptions", "IM后台命令状态选项", ImAdminCommandStatusOptions)
dict.RegisterEnums("ImAdminOutboxStatusOptions", "IM后台Outbox状态选项", ImAdminOutboxStatusOptions)
dict.RegisterEnums("ImKafkaDeadLetterStatusOptions", "IM Kafka死信状态选项", ImKafkaDeadLetterStatusOptions)
}
const (
ImKafkaSourceService = "hotgo-admin"
ImKafkaTargetService = "im-server"
ImAdminCommandStatusPending = "pending"
ImAdminCommandStatusRunning = "running"
ImAdminCommandStatusSucceeded = "succeeded"
ImAdminCommandStatusFailed = "failed"
ImAdminCommandStatusDead = "dead"
ImAdminOutboxStatusPending = "pending"
ImAdminOutboxStatusSent = "sent"
ImAdminOutboxStatusFailed = "failed"
ImAdminCommandGroupProfileUpdate = "group.profile.update"
ImAdminCommandGroupGovernUpdate = "group.governance.update"
ImAdminCommandGroupAdminSet = "group.admin.set"
ImAdminCommandGroupAdminRemove = "group.admin.remove"
ImAdminCommandGroupMemberMute = "group.member.mute"
ImAdminCommandGroupMemberUnmute = "group.member.unmute"
ImAdminCommandGroupMemberRemove = "group.member.remove"
ImAdminCommandGroupOwnerTransfer = "group.owner.transfer"
ImAdminCommandUserBan = "user.ban"
ImAdminCommandUserUnban = "user.unban"
ImAdminCommandUserFreeze = "user.freeze"
ImAdminCommandUserUnfreeze = "user.unfreeze"
ImAdminCommandUserDeviceKick = "user.device.kick"
ImAdminCommandUserLogoutAll = "user.logout_all"
ImAdminCommandUserCancel = "user.account.cancel"
ImAdminCommandUserDelete = "user.account.delete"
ImAdminCommandKafkaDeadReplay = "kafka.dead_letter.replay"
ImAdminCommandKafkaDeadIgnore = "kafka.dead_letter.ignore"
ImAdminCommandKafkaDeadResolve = "kafka.dead_letter.resolve"
ImKafkaDeadLetterStatusDead = "dead"
ImKafkaDeadLetterStatusPendingReplay = "pending_replay"
ImKafkaDeadLetterStatusReplayed = "replayed"
ImKafkaDeadLetterStatusIgnored = "ignored"
ImKafkaDeadLetterStatusResolved = "resolved"
ImConfigEventTenantUpdated = "tenant.updated"
ImConfigEventTenantStatusChanged = "tenant.status.changed"
ImConfigEventTenantLimitsUpdated = "tenant.limits.updated"
ImConfigEventTenantFeaturesUpdated = "tenant.features.updated"
ImConfigEventDictionaryUpdated = "dictionary.updated"
ImConfigEventThirdPartyUpdated = "third_party.updated"
ImConfigEventRegistrationUpdated = "registration.config.updated"
)
var ImAdminCommandStatusOptions = []*model.Option{
dict.GenWarningOption(ImAdminCommandStatusPending, "待投递"),
dict.GenInfoOption(ImAdminCommandStatusRunning, "执行中"),
dict.GenSuccessOption(ImAdminCommandStatusSucceeded, "成功"),
dict.GenErrorOption(ImAdminCommandStatusFailed, "失败"),
dict.GenErrorOption(ImAdminCommandStatusDead, "死信"),
}
var ImAdminOutboxStatusOptions = []*model.Option{
dict.GenWarningOption(ImAdminOutboxStatusPending, "待投递"),
dict.GenSuccessOption(ImAdminOutboxStatusSent, "已投递"),
dict.GenErrorOption(ImAdminOutboxStatusFailed, "投递失败"),
}
var ImKafkaDeadLetterStatusOptions = []*model.Option{
dict.GenErrorOption(ImKafkaDeadLetterStatusDead, "死信"),
dict.GenWarningOption(ImKafkaDeadLetterStatusPendingReplay, "等待重放"),
dict.GenSuccessOption(ImKafkaDeadLetterStatusReplayed, "已重放"),
dict.GenDefaultOption(ImKafkaDeadLetterStatusIgnored, "已忽略"),
dict.GenSuccessOption(ImKafkaDeadLetterStatusResolved, "已处理"),
}
+8
View File
@@ -10,4 +10,12 @@ const (
QueueLogTopic = `request_log` // 访问日志
QueueLoginLogTopic = `login_log` // 登录日志
QueueServeLogTopic = `serve_log` // 服务日志
QueueImAdminCommandTopic = `im.admin.command` // IM后台命令
QueueImAdminCommandResultTopic = `im.admin.command.result` // IM后台命令结果
QueueImConfigEventTopic = `im.config.event` // IM配置事件
QueueImCacheInvalidateTopic = `im.cache.invalidate` // IM缓存失效事件
QueueImWorkbenchEventTopic = `im.workbench.event` // IM工作台事件
QueueImDeadLetterTopic = `im.dead.letter` // IM死信事件
QueueImAuditEventTopic = `im.audit.event` // IM审计事件
)
@@ -0,0 +1,66 @@
// 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/api/admin/imdeadletter"
"hotgo/internal/service"
)
var ImDeadLetter = cImDeadLetter{}
type cImDeadLetter struct{}
func (c *cImDeadLetter) List(ctx context.Context, req *imdeadletter.ListReq) (res *imdeadletter.ListRes, err error) {
data, err := service.SysImDeadLetter().List(ctx, &req.ImDeadLetterListInp)
if err != nil {
return
}
res = new(imdeadletter.ListRes)
res.ImDeadLetterPageModel = data
return
}
func (c *cImDeadLetter) View(ctx context.Context, req *imdeadletter.ViewReq) (res *imdeadletter.ViewRes, err error) {
data, err := service.SysImDeadLetter().View(ctx, &req.ImDeadLetterViewInp)
if err != nil {
return
}
res = new(imdeadletter.ViewRes)
res.ImDeadLetterViewModel = data
return
}
func (c *cImDeadLetter) Replay(ctx context.Context, req *imdeadletter.ReplayReq) (res *imdeadletter.ReplayRes, err error) {
data, err := service.SysImDeadLetter().Replay(ctx, &req.ImDeadLetterActionInp)
if err != nil {
return
}
res = new(imdeadletter.ReplayRes)
res.ImGroupCommandModel = data
return
}
func (c *cImDeadLetter) Ignore(ctx context.Context, req *imdeadletter.IgnoreReq) (res *imdeadletter.IgnoreRes, err error) {
data, err := service.SysImDeadLetter().Ignore(ctx, &req.ImDeadLetterActionInp)
if err != nil {
return
}
res = new(imdeadletter.IgnoreRes)
res.ImGroupCommandModel = data
return
}
func (c *cImDeadLetter) Resolve(ctx context.Context, req *imdeadletter.ResolveReq) (res *imdeadletter.ResolveRes, err error) {
data, err := service.SysImDeadLetter().Resolve(ctx, &req.ImDeadLetterActionInp)
if err != nil {
return
}
res = new(imdeadletter.ResolveRes)
res.ImGroupCommandModel = data
return
}
@@ -149,3 +149,91 @@ func (c *cImGroup) AdminLogList(ctx context.Context, req *imgroup.AdminLogListRe
res.PageRes.Pack(req, totalCount)
return
}
// ProfileUpdate 通过Kafka修改IM群资料
func (c *cImGroup) ProfileUpdate(ctx context.Context, req *imgroup.ProfileUpdateReq) (res *imgroup.ProfileUpdateRes, err error) {
data, err := service.SysImGroup().ProfileUpdate(ctx, &req.ImGroupProfileUpdateInp)
if err != nil {
return
}
res = new(imgroup.ProfileUpdateRes)
res.ImGroupCommandModel = data
return
}
// GovernanceUpdate 通过Kafka修改IM群治理配置
func (c *cImGroup) GovernanceUpdate(ctx context.Context, req *imgroup.GovernanceUpdateReq) (res *imgroup.GovernanceUpdateRes, err error) {
data, err := service.SysImGroup().GovernanceUpdate(ctx, &req.ImGroupGovernanceUpdateInp)
if err != nil {
return
}
res = new(imgroup.GovernanceUpdateRes)
res.ImGroupCommandModel = data
return
}
// AdminSet 通过Kafka设置IM群管理员
func (c *cImGroup) AdminSet(ctx context.Context, req *imgroup.AdminSetReq) (res *imgroup.AdminSetRes, err error) {
data, err := service.SysImGroup().AdminSet(ctx, &req.ImGroupAdminSetInp)
if err != nil {
return
}
res = new(imgroup.AdminSetRes)
res.ImGroupCommandModel = data
return
}
// AdminRemove 通过Kafka删除IM群管理员
func (c *cImGroup) AdminRemove(ctx context.Context, req *imgroup.AdminRemoveReq) (res *imgroup.AdminRemoveRes, err error) {
data, err := service.SysImGroup().AdminRemove(ctx, &req.ImGroupAdminRemoveInp)
if err != nil {
return
}
res = new(imgroup.AdminRemoveRes)
res.ImGroupCommandModel = data
return
}
// MemberMute 通过Kafka禁言IM群成员
func (c *cImGroup) MemberMute(ctx context.Context, req *imgroup.MemberMuteReq) (res *imgroup.MemberMuteRes, err error) {
data, err := service.SysImGroup().MemberMute(ctx, &req.ImGroupMemberMuteInp)
if err != nil {
return
}
res = new(imgroup.MemberMuteRes)
res.ImGroupCommandModel = data
return
}
// MemberUnmute 通过Kafka解除IM群成员禁言
func (c *cImGroup) MemberUnmute(ctx context.Context, req *imgroup.MemberUnmuteReq) (res *imgroup.MemberUnmuteRes, err error) {
data, err := service.SysImGroup().MemberUnmute(ctx, &req.ImGroupMemberUnmuteInp)
if err != nil {
return
}
res = new(imgroup.MemberUnmuteRes)
res.ImGroupCommandModel = data
return
}
// MemberRemove 通过Kafka移出IM群成员
func (c *cImGroup) MemberRemove(ctx context.Context, req *imgroup.MemberRemoveReq) (res *imgroup.MemberRemoveRes, err error) {
data, err := service.SysImGroup().MemberRemove(ctx, &req.ImGroupMemberRemoveInp)
if err != nil {
return
}
res = new(imgroup.MemberRemoveRes)
res.ImGroupCommandModel = data
return
}
// OwnerTransfer 通过Kafka转让IM群主
func (c *cImGroup) OwnerTransfer(ctx context.Context, req *imgroup.OwnerTransferReq) (res *imgroup.OwnerTransferRes, err error) {
data, err := service.SysImGroup().OwnerTransfer(ctx, &req.ImGroupOwnerTransferInp)
if err != nil {
return
}
res = new(imgroup.OwnerTransferRes)
res.ImGroupCommandModel = data
return
}
@@ -0,0 +1,36 @@
package sys
import (
"context"
"hotgo/api/admin/imkafkaaudit"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImKafkaAudit = cImKafkaAudit{}
type cImKafkaAudit struct{}
func (c *cImKafkaAudit) List(ctx context.Context, req *imkafkaaudit.ListReq) (res *imkafkaaudit.ListRes, err error) {
list, totalCount, err := service.SysImKafkaAudit().List(ctx, &req.ImKafkaAuditListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImKafkaAuditListModel{}
}
res = new(imkafkaaudit.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImKafkaAudit) View(ctx context.Context, req *imkafkaaudit.ViewReq) (res *imkafkaaudit.ViewRes, err error) {
data, err := service.SysImKafkaAudit().View(ctx, &req.ImKafkaAuditViewInp)
if err != nil {
return
}
res = new(imkafkaaudit.ViewRes)
res.ImKafkaAuditViewModel = data
return
}
@@ -0,0 +1,61 @@
// 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/api/admin/imkafkacommand"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImKafkaCommand = cImKafkaCommand{}
type cImKafkaCommand struct{}
func (c *cImKafkaCommand) List(ctx context.Context, req *imkafkacommand.ListReq) (res *imkafkacommand.ListRes, err error) {
list, totalCount, err := service.SysImKafkaCommand().List(ctx, &req.ImKafkaCommandListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImKafkaCommandListModel{}
}
res = new(imkafkacommand.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImKafkaCommand) View(ctx context.Context, req *imkafkacommand.ViewReq) (res *imkafkacommand.ViewRes, err error) {
data, err := service.SysImKafkaCommand().View(ctx, &req.ImKafkaCommandViewInp)
if err != nil {
return
}
res = new(imkafkacommand.ViewRes)
res.ImKafkaCommandViewModel = data
return
}
func (c *cImKafkaCommand) CacheInvalidate(ctx context.Context, req *imkafkacommand.CacheInvalidateReq) (res *imkafkacommand.CacheInvalidateRes, err error) {
data, err := service.SysImKafkaCommand().CacheInvalidate(ctx, &req.ImKafkaCacheInvalidateInp)
if err != nil {
return
}
res = new(imkafkacommand.CacheInvalidateRes)
res.ImKafkaEventModel = data
return
}
func (c *cImKafkaCommand) WorkbenchEvent(ctx context.Context, req *imkafkacommand.WorkbenchEventReq) (res *imkafkacommand.WorkbenchEventRes, err error) {
data, err := service.SysImKafkaCommand().WorkbenchEvent(ctx, &req.ImKafkaWorkbenchEventInp)
if err != nil {
return
}
res = new(imkafkacommand.WorkbenchEventRes)
res.ImKafkaEventModel = data
return
}
@@ -46,3 +46,83 @@ func (c *cImUser) View(ctx context.Context, req *imuser.ViewReq) (res *imuser.Vi
res.ImUserViewModel = data
return
}
func (c *cImUser) Ban(ctx context.Context, req *imuser.BanReq) (res *imuser.BanRes, err error) {
data, err := service.SysImUser().Ban(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.BanRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) Unban(ctx context.Context, req *imuser.UnbanReq) (res *imuser.UnbanRes, err error) {
data, err := service.SysImUser().Unban(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.UnbanRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) Freeze(ctx context.Context, req *imuser.FreezeReq) (res *imuser.FreezeRes, err error) {
data, err := service.SysImUser().Freeze(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.FreezeRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) Unfreeze(ctx context.Context, req *imuser.UnfreezeReq) (res *imuser.UnfreezeRes, err error) {
data, err := service.SysImUser().Unfreeze(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.UnfreezeRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) DeviceKick(ctx context.Context, req *imuser.DeviceKickReq) (res *imuser.DeviceKickRes, err error) {
data, err := service.SysImUser().DeviceKick(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.DeviceKickRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) LogoutAll(ctx context.Context, req *imuser.LogoutAllReq) (res *imuser.LogoutAllRes, err error) {
data, err := service.SysImUser().LogoutAll(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.LogoutAllRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) AccountCancel(ctx context.Context, req *imuser.AccountCancelReq) (res *imuser.AccountCancelRes, err error) {
data, err := service.SysImUser().AccountCancel(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.AccountCancelRes)
res.ImUserCommandModel = data
return
}
func (c *cImUser) AccountDelete(ctx context.Context, req *imuser.AccountDeleteReq) (res *imuser.AccountDeleteRes, err error) {
data, err := service.SysImUser().AccountDelete(ctx, &req.ImUserGovernanceInp)
if err != nil {
return
}
res = new(imuser.AccountDeleteRes)
res.ImUserCommandModel = data
return
}
@@ -0,0 +1,144 @@
package imconfig
import (
"context"
api "hotgo/api/admin/imconfiginternal"
"hotgo/internal/dao"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"strings"
"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"
)
var Config = cConfig{}
type cConfig struct{}
func (c *cConfig) Tenant(ctx context.Context, req *api.TenantReq) (res *api.TenantRes, err error) {
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Code, req.Code)
if err != nil {
return nil, err
}
return &api.TenantRes{TenantModel: tenantToModel(tenant)}, nil
}
func (c *cConfig) TenantByID(ctx context.Context, req *api.TenantByIDReq) (res *api.TenantRes, err error) {
tenant, err := getTenant(ctx, dao.ImTenant.Columns().Id, req.Id)
if err != nil {
return nil, err
}
return &api.TenantRes{TenantModel: tenantToModel(tenant)}, nil
}
func (c *cConfig) TenantList(ctx context.Context, req *api.TenantListReq) (res *api.TenantListRes, err error) {
limit := req.Limit
if limit <= 0 || limit > 1000 {
limit = 1000
}
mod := imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Limit(limit)
if req.State != "" {
if req.State == "active" {
mod = mod.Where(dao.ImTenant.Columns().State, 1)
} else {
mod = mod.Where(dao.ImTenant.Columns().State, req.State)
}
}
var tenants []*entity.ImTenant
if err = mod.OrderAsc(dao.ImTenant.Columns().Id).Scan(&tenants); err != nil {
return nil, gerror.Wrap(err, "获取IM租户配置失败")
}
res = &api.TenantListRes{Items: make([]*api.TenantModel, 0, len(tenants))}
for _, tenant := range tenants {
res.Items = append(res.Items, tenantToModel(tenant))
}
return res, nil
}
func (c *cConfig) RegistrationSms(ctx context.Context, req *api.RegistrationSmsReq) (res *api.RegistrationSmsRes, err error) {
var record g.Map
if err = dao.SysConfig.Ctx(ctx).
Where(dao.SysConfig.Columns().Group, "im").
Where(dao.SysConfig.Columns().Key, "register_sms_enabled").
Where(dao.SysConfig.Columns().Status, 1).
Scan(&record); err != nil {
return nil, gerror.Wrap(err, "获取IM注册短信配置失败")
}
enabled := true
if record != nil {
enabled = gconv.String(record[dao.SysConfig.Columns().Value]) != "0"
}
return &api.RegistrationSmsRes{Enabled: enabled}, nil
}
func (c *cConfig) ThirdParty(ctx context.Context, req *api.ThirdPartyReq) (res *api.ThirdPartyRes, err error) {
values := map[string]string{}
keys := splitKeys(req.Keys)
mod := dao.SysConfig.Ctx(ctx).Where(dao.SysConfig.Columns().Group, req.Group).Where(dao.SysConfig.Columns().Status, 1)
if len(keys) > 0 {
mod = mod.WhereIn(dao.SysConfig.Columns().Key, keys)
}
var rows []*entity.SysConfig
if err = mod.Scan(&rows); err != nil {
return nil, gerror.Wrap(err, "获取IM第三方配置失败")
}
for _, row := range rows {
values[row.Key] = row.Value
}
return &api.ThirdPartyRes{Values: values}, nil
}
func getTenant(ctx context.Context, field string, value any) (*entity.ImTenant, error) {
var tenant *entity.ImTenant
if err := imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Where(field, value).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取IM租户配置失败")
}
if tenant == nil || tenant.Id <= 0 {
return nil, gerror.New("IM租户不存在")
}
return tenant, nil
}
func tenantToModel(tenant *entity.ImTenant) *api.TenantModel {
if tenant == nil {
return nil
}
desc := tenant.TenantDesc
return &api.TenantModel{
ID: tenant.Id,
Code: tenant.Code,
Name: tenant.Name,
TenantDesc: &desc,
DBName: tenant.DbName,
DBDSN: tenant.DbDsn,
State: tenant.State,
CreatedAt: gtimeToRFC3339(tenant.CreatedAt),
CreatedBy: tenant.CreatedBy,
UpdatedAt: gtimeToRFC3339(tenant.UpdatedAt),
UpdatedBy: tenant.UpdatedBy,
}
}
func gtimeToRFC3339(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Time.Format("2006-01-02T15:04:05Z07:00")
}
func splitKeys(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
keys := make([]string, 0, len(parts))
for _, part := range parts {
if part = strings.TrimSpace(part); part != "" {
keys = append(keys, part)
}
}
return keys
}
+38
View File
@@ -0,0 +1,38 @@
// Package crons
// @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 crons
import (
"context"
"hotgo/internal/library/cron"
"hotgo/internal/library/imkafka"
)
func init() {
cron.Register(ImAdminOutbox)
}
var ImAdminOutbox = &cImAdminOutbox{name: "im_admin_outbox"}
type cImAdminOutbox struct {
name string
}
func (c *cImAdminOutbox) GetName() string {
return c.name
}
func (c *cImAdminOutbox) Execute(ctx context.Context, parser *cron.Parser) (err error) {
sent, err := imkafka.DispatchPendingOutbox(ctx, 100)
if err != nil {
parser.Logger.Warningf(ctx, "IM后台命令Outbox补偿投递失败 err:%+v", err)
return err
}
if sent > 0 {
parser.Logger.Infof(ctx, "IM后台命令Outbox补偿投递完成 count:%d", sent)
}
return nil
}
+26 -8
View File
@@ -10,10 +10,13 @@ import (
)
type Config struct {
BaseUrl string `json:"baseUrl"`
ServiceId string `json:"serviceId"`
ServiceSecret string `json:"serviceSecret"`
Timeout time.Duration `json:"timeout"`
BaseUrl string `json:"baseUrl"`
ServiceId string `json:"serviceId"`
ServiceSecret string `json:"serviceSecret"`
Timeout time.Duration `json:"timeout"`
InternalBaseUrl string `json:"internalBaseUrl"`
InternalServiceName string `json:"internalServiceName"`
InternalServiceSecret string `json:"internalServiceSecret"`
}
func LoadConfig(ctx context.Context) Config {
@@ -28,10 +31,25 @@ func LoadConfig(ctx context.Context) Config {
if serviceSecret == "" {
serviceSecret = os.Getenv("IM_ADMIN_API_SECRET")
}
internalSecret := g.Cfg().MustGet(ctx, "imInternalApi.serviceSecret").String()
if strings.HasPrefix(internalSecret, "${") && strings.HasSuffix(internalSecret, "}") {
internalSecret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(internalSecret, "${"), "}"))
}
if internalSecret == "" {
internalSecret = os.Getenv("IM_INTERNAL_API_SECRET")
}
if internalSecret == "" {
internalSecret = serviceSecret
}
baseUrl := g.Cfg().MustGet(ctx, "imAdminApi.baseUrl").String()
serviceId := g.Cfg().MustGet(ctx, "imAdminApi.serviceId", "hotgo-admin").String()
return Config{
BaseUrl: g.Cfg().MustGet(ctx, "imAdminApi.baseUrl").String(),
ServiceId: g.Cfg().MustGet(ctx, "imAdminApi.serviceId", "hotgo-admin").String(),
ServiceSecret: serviceSecret,
Timeout: timeout,
BaseUrl: baseUrl,
ServiceId: serviceId,
ServiceSecret: serviceSecret,
Timeout: timeout,
InternalBaseUrl: g.Cfg().MustGet(ctx, "imInternalApi.baseUrl", baseUrl).String(),
InternalServiceName: g.Cfg().MustGet(ctx, "imInternalApi.serviceName", serviceId).String(),
InternalServiceSecret: internalSecret,
}
}
+101
View File
@@ -0,0 +1,101 @@
package imadmin
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/errors/gerror"
)
func (c *Client) InternalGet(ctx context.Context, path string, out any) error {
req, err := c.newInternalRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return gerror.New("IM内部查询服务暂不可用")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return c.parseError(resp)
}
if out == nil {
return nil
}
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
if err != nil {
return gerror.Wrap(err, "读取IM内部查询响应失败")
}
var envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Reason string `json:"reason"`
Data json.RawMessage `json:"data"`
}
if err = json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 {
if envelope.Code != 0 {
return stableError(envelope.Message, envelope.Reason)
}
return json.Unmarshal(envelope.Data, out)
}
return json.Unmarshal(body, out)
}
func (c *Client) newInternalRequest(ctx context.Context, method string, path string, bodyBytes []byte) (*http.Request, error) {
baseUrl := strings.TrimRight(c.conf.InternalBaseUrl, "/")
if baseUrl == "" {
baseUrl = strings.TrimRight(c.conf.BaseUrl, "/")
}
if baseUrl == "" {
return nil, gerror.New("IM内部查询API未配置")
}
serviceName := strings.TrimSpace(c.conf.InternalServiceName)
if serviceName == "" {
serviceName = strings.TrimSpace(c.conf.ServiceId)
}
if serviceName == "" {
return nil, gerror.New("IM内部查询服务名未配置")
}
secret := c.conf.InternalServiceSecret
if secret == "" {
secret = c.conf.ServiceSecret
}
if secret == "" {
return nil, gerror.New("IM内部查询API密钥未配置")
}
req, err := http.NewRequestWithContext(ctx, method, baseUrl+path, bytes.NewReader(bodyBytes))
if err != nil {
return nil, gerror.Wrap(err, "创建IM内部查询请求失败")
}
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
nonce, err := randomNonce()
if err != nil {
return nil, err
}
bodyHash := sha256.Sum256(bodyBytes)
signingText := strings.Join([]string{
method,
requestTarget(req),
hex.EncodeToString(bodyHash[:]),
timestamp,
nonce,
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(signingText))
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Service-Name", serviceName)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
return req, nil
}
+135
View File
@@ -0,0 +1,135 @@
package iminternal
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
const (
headerService = "X-Service-Name"
headerTimestamp = "X-Timestamp"
headerNonce = "X-Nonce"
headerSignature = "X-Signature"
)
var nonces sync.Map
func Middleware(r *ghttp.Request) {
if !Authorize(r) {
return
}
r.Middleware.Next()
}
func Authorize(r *ghttp.Request) bool {
ctx := r.GetCtx()
serviceName := strings.TrimSpace(r.Header.Get(headerService))
timestampRaw := strings.TrimSpace(r.Header.Get(headerTimestamp))
nonce := strings.TrimSpace(r.Header.Get(headerNonce))
signature := strings.TrimSpace(r.Header.Get(headerSignature))
expectedService := g.Cfg().MustGet(ctx, "imConfigApi.serviceName", "im-server").String()
secret := configuredSecret(r)
if serviceName == "" || timestampRaw == "" || nonce == "" || signature == "" || serviceName != expectedService || secret == "" {
writeAuthError(r, http.StatusUnauthorized, "internal signature required")
return false
}
timestamp, err := strconv.ParseInt(timestampRaw, 10, 64)
if err != nil {
writeAuthError(r, http.StatusUnauthorized, "invalid internal timestamp")
return false
}
window := g.Cfg().MustGet(ctx, "imConfigApi.timeWindow", "300s").Duration()
if window <= 0 {
window = 5 * time.Minute
}
delta := time.Since(time.Unix(timestamp, 0))
if delta > window || delta < -window {
writeAuthError(r, http.StatusUnauthorized, "internal signature expired")
return false
}
body, err := readBody(r)
if err != nil {
writeAuthError(r, http.StatusInternalServerError, "read internal body failed")
return false
}
bodyHash := sha256.Sum256(body)
signingText := strings.Join([]string{
r.Method,
r.URL.RequestURI(),
hex.EncodeToString(bodyHash[:]),
timestampRaw,
nonce,
}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(signingText))
if !hmac.Equal([]byte(strings.ToLower(signature)), []byte(hex.EncodeToString(mac.Sum(nil)))) {
writeAuthError(r, http.StatusUnauthorized, "invalid internal signature")
return false
}
if !claimNonce(serviceName, nonce, time.Now().Add(window)) {
writeAuthError(r, http.StatusUnauthorized, "internal nonce replayed")
return false
}
return true
}
func configuredSecret(r *ghttp.Request) string {
ctx := r.GetCtx()
secret := g.Cfg().MustGet(ctx, "imConfigApi.serviceSecret").String()
if strings.HasPrefix(secret, "${") && strings.HasSuffix(secret, "}") {
secret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(secret, "${"), "}"))
}
if secret == "" {
secret = os.Getenv("IM_CONFIG_API_SECRET")
}
if secret == "" {
secret = g.Cfg().MustGet(ctx, "imInternalApi.serviceSecret").String()
}
return secret
}
func readBody(r *ghttp.Request) ([]byte, error) {
if r.Body == nil {
return nil, nil
}
body, err := io.ReadAll(io.LimitReader(r.Body, 4<<20))
if err != nil {
return nil, err
}
r.Body = io.NopCloser(bytes.NewReader(body))
return body, nil
}
func claimNonce(serviceName string, nonce string, expiresAt time.Time) bool {
now := time.Now()
nonces.Range(func(key, value any) bool {
if expires, ok := value.(time.Time); ok && expires.Before(now) {
nonces.Delete(key)
}
return true
})
key := serviceName + ":" + nonce
_, loaded := nonces.LoadOrStore(key, expiresAt)
return !loaded
}
func writeAuthError(r *ghttp.Request, status int, message string) {
r.Response.WriteStatus(status)
r.Response.WriteJson(g.Map{
"code": status,
"message": message,
})
r.Exit()
}
+378
View File
@@ -0,0 +1,378 @@
package imkafka
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/queue"
"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/gconv"
"github.com/gogf/gf/v2/util/guid"
)
const (
adminCommandTable = "im_admin_commands"
adminOutboxTable = "im_admin_outbox"
)
func PublishAdminCommand(ctx context.Context, in AdminCommandInput) (res *AdminCommandModel, err error) {
if in.TenantID <= 0 || in.TenantCode == "" {
return nil, gerror.New("IM租户信息不完整")
}
if in.EventType == "" {
return nil, gerror.New("IM命令类型不能为空")
}
if in.IdempotencyKey == "" {
in.IdempotencyKey = "cmd_" + guid.S()
}
commandID := "cmd_" + guid.S()
eventID := "evt_" + guid.S()
now := time.Now().UTC().Round(time.Millisecond)
payload := in.Payload
if payload == nil {
payload = map[string]any{}
}
payload["command_id"] = commandID
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, gerror.Wrap(err, "序列化IM命令Payload失败")
}
envelope := Envelope{
EventID: eventID,
TraceID: gctx.CtxId(ctx),
TenantCode: in.TenantCode,
SourceService: consts.ImKafkaSourceService,
TargetService: consts.ImKafkaTargetService,
EventType: in.EventType,
BizType: in.BizType,
BizID: in.BizID,
IdempotencyKey: in.IdempotencyKey,
Version: in.Version,
OccurredAt: now,
Actor: in.Actor,
Payload: payloadBytes,
}
envelopeBytes, err := json.Marshal(envelope)
if err != nil {
return nil, gerror.Wrap(err, "序列化IM命令Envelope失败")
}
db := g.DB()
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
data := g.Map{
"command_id": commandID,
"tenant_id": in.TenantID,
"tenant_code": in.TenantCode,
"event_id": eventID,
"event_type": in.EventType,
"biz_type": in.BizType,
"biz_id": in.BizID,
"idempotency_key": in.IdempotencyKey,
"status": consts.ImAdminCommandStatusPending,
"payload": string(payloadBytes),
"envelope": string(envelopeBytes),
"created_by": in.CreatedBy,
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}
if _, err := tx.Model(adminCommandTable).Safe().Ctx(ctx).Data(data).Insert(); err != nil {
return gerror.Wrap(err, "创建IM后台命令失败")
}
outbox := g.Map{
"event_id": eventID,
"command_id": commandID,
"topic": consts.QueueImAdminCommandTopic,
"event_type": in.EventType,
"biz_type": in.BizType,
"biz_id": in.BizID,
"key_value": in.BizID,
"envelope": string(envelopeBytes),
"status": consts.ImAdminOutboxStatusPending,
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}
if _, err := tx.Model(adminOutboxTable).Safe().Ctx(ctx).Data(outbox).Insert(); err != nil {
return gerror.Wrap(err, "创建IM后台命令Outbox失败")
}
return nil
})
if err != nil {
return nil, err
}
res = &AdminCommandModel{
CommandID: commandID,
EventID: eventID,
Status: consts.ImAdminCommandStatusPending,
TenantID: in.TenantID,
TenantCode: in.TenantCode,
EventType: in.EventType,
IdempotencyKey: in.IdempotencyKey,
}
if err = DispatchOutboxByEvent(ctx, eventID); err != nil {
return res, err
}
res.Status = consts.ImAdminCommandStatusRunning
return res, nil
}
func DispatchOutboxByEvent(ctx context.Context, eventID string) error {
var row gdb.Record
if err := g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).
Where("event_id", eventID).
Where("status", consts.ImAdminOutboxStatusPending).
Scan(&row); err != nil {
return gerror.Wrap(err, "读取IM命令Outbox失败")
}
if row.IsEmpty() {
return nil
}
return dispatchOutboxRecord(ctx, row)
}
func PublishConfigEvent(ctx context.Context, in ConfigEventInput) error {
if in.EventType == "" {
return gerror.New("IM配置事件类型不能为空")
}
eventID := "evt_" + guid.S()
now := time.Now().UTC().Round(time.Millisecond)
payload := map[string]any{
"tenant_code": in.TenantCode,
"tenant_id": in.TenantID,
"group": in.Group,
"keys": in.Keys,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return gerror.Wrap(err, "序列化IM配置事件Payload失败")
}
envelope := Envelope{
EventID: eventID,
TraceID: gctx.CtxId(ctx),
TenantCode: in.TenantCode,
SourceService: consts.ImKafkaSourceService,
TargetService: consts.ImKafkaTargetService,
EventType: in.EventType,
BizType: "config",
BizID: in.BizID,
OccurredAt: now,
Actor: in.Actor,
Payload: payloadBytes,
}
envelopeBytes, err := json.Marshal(envelope)
if err != nil {
return gerror.Wrap(err, "序列化IM配置事件Envelope失败")
}
_, err = g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).Data(g.Map{
"event_id": eventID,
"command_id": "",
"topic": consts.QueueImConfigEventTopic,
"event_type": in.EventType,
"biz_type": "config",
"biz_id": in.BizID,
"key_value": in.TenantCode,
"envelope": string(envelopeBytes),
"status": consts.ImAdminOutboxStatusPending,
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert()
if err != nil {
return gerror.Wrap(err, "创建IM配置事件Outbox失败")
}
return DispatchOutboxByEvent(ctx, eventID)
}
func PublishEvent(ctx context.Context, in EventInput) (eventID string, err error) {
if in.Topic == "" {
return "", gerror.New("IM Kafka Topic不能为空")
}
if in.EventType == "" {
return "", gerror.New("IM Kafka事件类型不能为空")
}
eventID = "evt_" + guid.S()
now := time.Now().UTC().Round(time.Millisecond)
payload := in.Payload
if payload == nil {
payload = map[string]any{}
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return "", gerror.Wrap(err, "序列化IM Kafka事件Payload失败")
}
targetService := in.TargetService
if targetService == "" {
targetService = consts.ImKafkaTargetService
}
envelope := Envelope{
EventID: eventID,
TraceID: gctx.CtxId(ctx),
TenantCode: in.TenantCode,
SourceService: consts.ImKafkaSourceService,
TargetService: targetService,
EventType: in.EventType,
BizType: in.BizType,
BizID: in.BizID,
IdempotencyKey: in.IdempotencyKey,
Version: in.Version,
OccurredAt: now,
Actor: in.Actor,
Payload: payloadBytes,
}
envelopeBytes, err := json.Marshal(envelope)
if err != nil {
return "", gerror.Wrap(err, "序列化IM Kafka事件Envelope失败")
}
_, err = g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).Data(g.Map{
"event_id": eventID,
"command_id": "",
"topic": in.Topic,
"event_type": in.EventType,
"biz_type": in.BizType,
"biz_id": in.BizID,
"key_value": in.BizID,
"envelope": string(envelopeBytes),
"status": consts.ImAdminOutboxStatusPending,
"created_at": gtime.Now(),
"updated_at": gtime.Now(),
}).Insert()
if err != nil {
return "", gerror.Wrap(err, "创建IM Kafka事件Outbox失败")
}
if err = DispatchOutboxByEvent(ctx, eventID); err != nil {
return eventID, err
}
return eventID, nil
}
func DispatchPendingOutbox(ctx context.Context, limit int) (sent int, err error) {
if limit <= 0 {
limit = 100
}
var rows []gdb.Record
err = g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).
WhereIn("status", []string{consts.ImAdminOutboxStatusPending, consts.ImAdminOutboxStatusFailed}).
Where("next_retry_at IS NULL OR next_retry_at <= NOW()").
OrderAsc("id").
Limit(limit).
Scan(&rows)
if err != nil {
return 0, gerror.Wrap(err, "读取待投递IM命令Outbox失败")
}
for _, row := range rows {
if err = dispatchOutboxRecord(ctx, row); err != nil {
g.Log().Warningf(ctx, "IM命令Outbox投递失败 event:%s err:%+v", row["event_id"].String(), err)
continue
}
sent++
}
return sent, nil
}
func dispatchOutboxRecord(ctx context.Context, row gdb.Record) error {
eventID := row["event_id"].String()
commandID := row["command_id"].String()
topic := row["topic"].String()
body := row["envelope"].String()
if topic == "" || body == "" {
return markOutboxFailed(ctx, eventID, "MALFORMED_OUTBOX", "topic or envelope is empty")
}
if g.Cfg().MustGet(ctx, "queue.driver").String() != "kafka" {
err := gerror.New("IM Kafka未启用,请将queue.driver配置为kafka")
_ = markOutboxFailed(ctx, eventID, "KAFKA_DISABLED", err.Error())
return err
}
if err := queue.Push(topic, body); err != nil {
_ = markOutboxFailed(ctx, eventID, "KAFKA_SEND_FAILED", err.Error())
return err
}
now := gtime.Now()
_, err := g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).
Where("event_id", eventID).
Data(g.Map{
"status": consts.ImAdminOutboxStatusSent,
"attempts": gdb.Raw("attempts + 1"),
"sent_at": now,
"last_reason": "",
"last_error": "",
"updated_at": now,
}).Update()
if err != nil {
return gerror.Wrap(err, "更新IM命令Outbox投递状态失败")
}
if commandID != "" {
_, err = g.DB().Model(adminCommandTable).Safe().Ctx(ctx).
Where("command_id", commandID).
Data(g.Map{
"status": consts.ImAdminCommandStatusRunning,
"sent_at": now,
"updated_at": now,
}).Update()
if err != nil {
return gerror.Wrap(err, "更新IM后台命令投递状态失败")
}
}
return nil
}
func markOutboxFailed(ctx context.Context, eventID string, reason string, message string) error {
now := gtime.Now()
_, err := g.DB().Model(adminOutboxTable).Safe().Ctx(ctx).
Where("event_id", eventID).
Data(g.Map{
"status": consts.ImAdminOutboxStatusFailed,
"attempts": gdb.Raw("attempts + 1"),
"next_retry_at": now.Add(30 * time.Second),
"last_reason": reason,
"last_error": message,
"updated_at": now,
}).Update()
if err != nil {
return gerror.Wrap(err, "更新IM命令Outbox失败状态失败")
}
return nil
}
func ApplyCommandResult(ctx context.Context, result CommandResult) error {
commandID := result.CommandID
if commandID == "" {
commandID = result.CommandEventID
}
if commandID == "" && result.CommandEventID == "" {
return gerror.New("IM命令结果缺少command_id")
}
status := gconv.String(result.Status)
switch status {
case consts.ImAdminCommandStatusSucceeded, consts.ImAdminCommandStatusFailed, consts.ImAdminCommandStatusDead:
default:
status = consts.ImAdminCommandStatusFailed
}
data := g.Map{
"status": status,
"reason": result.Reason,
"message": result.Message,
"completed_at": gtime.Now(),
"updated_at": gtime.Now(),
}
if len(result.Result) > 0 {
data["result"] = string(result.Result)
}
mod := g.DB().Model(adminCommandTable).Safe().Ctx(ctx)
if result.CommandID != "" {
mod = mod.Where("command_id", result.CommandID)
} else {
mod = mod.Where("event_id", result.CommandEventID)
}
_, err := mod.Data(data).Update()
if err != nil {
return gerror.Wrap(err, "更新IM后台命令结果失败")
}
return nil
}
@@ -0,0 +1,90 @@
package imkafka
import (
"encoding/json"
"time"
)
type Actor struct {
Type string `json:"type,omitempty"`
ID string `json:"id,omitempty"`
DisplayName string `json:"display_name,omitempty"`
}
type Envelope struct {
EventID string `json:"event_id"`
TraceID string `json:"trace_id,omitempty"`
TenantCode string `json:"tenant_code,omitempty"`
SourceService string `json:"source_service,omitempty"`
TargetService string `json:"target_service,omitempty"`
EventType string `json:"event_type"`
BizType string `json:"biz_type,omitempty"`
BizID string `json:"biz_id,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
Version int64 `json:"version,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
Actor *Actor `json:"actor,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
ParentEventID string `json:"parent_event_id,omitempty"`
}
type CommandResult struct {
EventID string `json:"event_id"`
TraceID string `json:"trace_id,omitempty"`
TenantCode string `json:"tenant_code,omitempty"`
CommandID string `json:"command_id"`
CommandEventID string `json:"command_event_id"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Message string `json:"message,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
}
type AdminCommandInput struct {
TenantID int64
TenantCode string
EventType string
BizType string
BizID string
IdempotencyKey string
Version int64
Actor *Actor
Payload map[string]any
CreatedBy int64
}
type ConfigEventInput struct {
TenantID int64
TenantCode string
EventType string
BizID string
Group string
Keys []string
Actor *Actor
CreatedBy int64
}
type EventInput struct {
Topic string
TenantCode string
TargetService string
EventType string
BizType string
BizID string
IdempotencyKey string
Version int64
Actor *Actor
Payload map[string]any
CreatedBy int64
}
type AdminCommandModel struct {
CommandID string `json:"commandId" dc:"命令ID"`
EventID string `json:"eventId" dc:"Kafka事件ID"`
Status string `json:"status" dc:"状态"`
TenantID int64 `json:"tenantId" dc:"租户ID"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventType string `json:"eventType" dc:"事件类型"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
+1 -1
View File
@@ -125,7 +125,7 @@ func (d *DiskProducerMq) getProducer(topic string) *disk.Queue {
func NewDiskQueue(topic string, config *disk.Config) *disk.Queue {
conf := &disk.Config{
Path: fmt.Sprintf(config.Path + "/" + config.GroupName + "/" + topic),
Path: fmt.Sprintf("%s/%s/%s", config.Path, config.GroupName, topic),
BatchSize: config.BatchSize,
BatchTime: config.BatchTime * time.Second,
SegmentSize: config.SegmentSize,
+81 -20
View File
@@ -7,13 +7,16 @@ package queue
import (
"context"
"fmt"
"crypto/tls"
"strings"
"time"
"github.com/IBM/sarama"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/xdg-go/scram"
"hotgo/internal/consts"
"hotgo/utility/simple"
"time"
)
type KafkaMq struct {
@@ -23,14 +26,17 @@ type KafkaMq struct {
}
type KafkaConfig struct {
ClientId string
Brokers []string
GroupID string
Partitions int32
Replication int16
Version string
UserName string
Password string
ClientId string
Brokers []string
GroupID string
Partitions int32
Replication int16
Version string
UserName string
Password string
SASLEnabled bool
SASLMechanism string
TLSEnabled bool
}
// SendMsg 按字符串类型生产数据
@@ -98,7 +104,7 @@ func (r *KafkaMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg))
}
if consumerCtx.Err() != nil {
Logger().Debugf(ctx, fmt.Sprintf("kafka consoumer stop : %v", consumerCtx.Err()))
Logger().Debugf(ctx, "kafka consoumer stop : %v", consumerCtx.Err())
return
}
consumer.ready = make(chan bool)
@@ -134,10 +140,8 @@ func RegisterKafkaMqConsumer(connOpt KafkaConfig) (client MqConsumer, err error)
conf := sarama.NewConfig()
conf.Consumer.Return.Errors = true
conf.Version = kfkVersion
if connOpt.UserName != "" {
conf.Net.SASL.Enable = true
conf.Net.SASL.User = connOpt.UserName
conf.Net.SASL.Password = connOpt.Password
if err = configureKafkaSecurity(conf, connOpt); err != nil {
return
}
// 默认按随机方式消费
@@ -157,7 +161,9 @@ func RegisterKafkaMqConsumer(connOpt KafkaConfig) (client MqConsumer, err error)
// RegisterKafkaProducer 注册并启动生产者接口实现
func RegisterKafkaProducer(connOpt KafkaConfig) (client MqProducer, err error) {
mqIns := &KafkaMq{}
connOpt.ClientId = "HOTGO-Producer"
if connOpt.ClientId == "" {
connOpt.ClientId = "hotgo-producer"
}
// 这里如果使用go程需要处理chan同步问题
if err = doRegisterKafkaProducer(connOpt, mqIns); err != nil {
@@ -191,10 +197,8 @@ func doRegisterKafkaProducer(connOpt KafkaConfig, mqIns *KafkaMq) (err error) {
conf.ClientID = connOpt.ClientId
conf.Version = kfkVersion
if connOpt.UserName != "" {
conf.Net.SASL.Enable = true
conf.Net.SASL.User = connOpt.UserName
conf.Net.SASL.Password = connOpt.Password
if err = configureKafkaSecurity(conf, connOpt); err != nil {
return
}
mqIns.producerIns, err = sarama.NewAsyncProducer(brokers, conf)
@@ -209,6 +213,63 @@ func doRegisterKafkaProducer(connOpt KafkaConfig, mqIns *KafkaMq) (err error) {
return
}
func configureKafkaSecurity(conf *sarama.Config, connOpt KafkaConfig) error {
if connOpt.TLSEnabled {
conf.Net.TLS.Enable = true
conf.Net.TLS.Config = &tls.Config{MinVersion: tls.VersionTLS12}
}
if !connOpt.SASLEnabled && connOpt.UserName == "" {
return nil
}
if connOpt.UserName == "" || connOpt.Password == "" {
return gerror.New("queue kafka sasl username or password is empty")
}
conf.Net.SASL.Enable = true
conf.Net.SASL.User = connOpt.UserName
conf.Net.SASL.Password = connOpt.Password
mechanism := strings.ToUpper(strings.ReplaceAll(connOpt.SASLMechanism, "_", "-"))
switch mechanism {
case "", "PLAIN":
conf.Net.SASL.Mechanism = sarama.SASLTypePlaintext
case "SCRAM-SHA-256":
conf.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA256
conf.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient {
return &xdgSCRAMClient{HashGeneratorFcn: scram.SHA256}
}
case "SCRAM-SHA-512":
conf.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA512
conf.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient {
return &xdgSCRAMClient{HashGeneratorFcn: scram.SHA512}
}
default:
return gerror.New("queue kafka sasl mechanism is not support")
}
return nil
}
type xdgSCRAMClient struct {
*scram.Client
*scram.ClientConversation
scram.HashGeneratorFcn
}
func (x *xdgSCRAMClient) Begin(userName, password, authzID string) (err error) {
x.Client, err = x.HashGeneratorFcn.NewClient(userName, password, authzID)
if err != nil {
return err
}
x.ClientConversation = x.Client.NewConversation()
return nil
}
func (x *xdgSCRAMClient) Step(challenge string) (response string, err error) {
return x.ClientConversation.Step(challenge)
}
func (x *xdgSCRAMClient) Done() bool {
return x.ClientConversation.Done()
}
// validateVersion 验证版本是否有效
func validateVersion(version sarama.KafkaVersion) bool {
for _, item := range sarama.SupportedVersions {
+51 -7
View File
@@ -11,6 +11,8 @@ import (
"github.com/gogf/gf/v2/os/gctx"
"hotgo/internal/library/queue/disk"
"hotgo/utility/charset"
"os"
"strings"
"sync"
"time"
)
@@ -59,6 +61,12 @@ type KafkaConf struct {
Version string `json:"version"`
RandClient bool `json:"randClient"`
MultiConsumer bool `json:"multiConsumer"`
ClientId string `json:"clientId"`
UserName string `json:"userName"`
Password string `json:"password"`
SASLEnabled bool `json:"saslEnabled"`
SASLMechanism string `json:"saslMechanism"`
TLSEnabled bool `json:"tlsEnabled"`
}
type MqMsg struct {
@@ -85,6 +93,7 @@ func init() {
if err := g.Cfg().MustGet(ctx, "queue").Scan(&config); err != nil {
Logger().Warningf(ctx, "queue init err:%+v", err)
}
normalizeKafkaConf()
}
// InstanceConsumer 实例化消费者
@@ -121,9 +130,15 @@ func NewProducer(groupName string) (mqClient MqProducer, err error) {
return
}
mqClient, err = RegisterKafkaProducer(KafkaConfig{
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
ClientId: firstKafkaClientId(config.Kafka.ClientId, "hotgo-producer"),
UserName: config.Kafka.UserName,
Password: config.Kafka.Password,
SASLEnabled: config.Kafka.SASLEnabled,
SASLMechanism: config.Kafka.SASLMechanism,
TLSEnabled: config.Kafka.TLSEnabled,
})
case "redis":
if _, err = g.Redis().Do(ctx, "ping"); err == nil {
@@ -185,10 +200,15 @@ func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
}
mqClient, err = RegisterKafkaMqConsumer(KafkaConfig{
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
ClientId: clientId,
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
ClientId: clientId,
UserName: config.Kafka.UserName,
Password: config.Kafka.Password,
SASLEnabled: config.Kafka.SASLEnabled,
SASLMechanism: config.Kafka.SASLMechanism,
TLSEnabled: config.Kafka.TLSEnabled,
})
case "redis":
if _, err = g.Redis().Do(ctx, "ping"); err == nil {
@@ -213,6 +233,30 @@ func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
return
}
func normalizeKafkaConf() {
config.Kafka.UserName = resolveEnvPlaceholder(config.Kafka.UserName)
config.Kafka.Password = resolveEnvPlaceholder(config.Kafka.Password)
if config.Kafka.SASLMechanism == "" {
config.Kafka.SASLMechanism = "SCRAM-SHA-512"
}
}
func resolveEnvPlaceholder(value string) string {
value = strings.TrimSpace(value)
if strings.HasPrefix(value, "${") && strings.HasSuffix(value, "}") {
return os.Getenv(strings.TrimSuffix(strings.TrimPrefix(value, "${"), "}"))
}
return value
}
func firstKafkaClientId(value string, fallback string) string {
value = strings.TrimSpace(value)
if value != "" {
return value
}
return fallback
}
// BodyString 返回消息体
func (m *MqMsg) BodyString() string {
return string(m.Body)
+203
View File
@@ -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,
}
}
+222
View File
@@ -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
}
+38 -1
View File
@@ -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)
}
}
+101
View File
@@ -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",
@@ -0,0 +1,130 @@
// Package sysin
// @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 sysin
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImDeadLetterListInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
Status string `json:"status" dc:"状态"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
EventType string `json:"eventType" dc:"事件类型"`
Cursor string `json:"cursor" dc:"游标"`
Limit int `json:"limit" dc:"数量"`
}
func (in *ImDeadLetterListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择IM租户")
}
if in.Limit <= 0 || in.Limit > 200 {
in.Limit = 50
}
return nil
}
type ImDeadLetterListModel struct {
Id int64 `json:"id" dc:"ID"`
DeadId string `json:"deadId" dc:"死信ID"`
EventId string `json:"eventId" dc:"事件ID"`
ParentEventId string `json:"parentEventId" dc:"父事件ID"`
SourceTopic string `json:"sourceTopic" dc:"来源Topic"`
ConsumerGroup string `json:"consumerGroup" dc:"消费组"`
EventType string `json:"eventType" dc:"事件类型"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
RetryCount int `json:"retryCount" dc:"重试次数"`
LastReason string `json:"lastReason" dc:"最后原因"`
LastError string `json:"lastError" dc:"最后错误"`
Status string `json:"status" dc:"状态"`
ReplayCount int `json:"replayCount" dc:"重放次数"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImDeadLetterListModelRaw struct {
Id int64 `json:"id"`
DeadId string `json:"dead_id"`
EventId string `json:"event_id"`
ParentEventId string `json:"parent_event_id"`
SourceTopic string `json:"source_topic"`
ConsumerGroup string `json:"consumer_group"`
EventType string `json:"event_type"`
BizType string `json:"biz_type"`
BizId string `json:"biz_id"`
RetryCount int `json:"retry_count"`
LastReason string `json:"last_reason"`
LastError string `json:"last_error"`
Status string `json:"status"`
ReplayCount int `json:"replay_count"`
CreatedAt *gtime.Time `json:"created_at"`
UpdatedAt *gtime.Time `json:"updated_at"`
}
type ImDeadLetterPageModel struct {
List []*ImDeadLetterListModel `json:"list" dc:"数据列表"`
NextCursor string `json:"nextCursor" dc:"下一页游标"`
HasMore bool `json:"hasMore" dc:"是否还有更多"`
}
type ImDeadLetterPageRaw struct {
Items []*ImDeadLetterListModelRaw `json:"items"`
NextCursor string `json:"next_cursor"`
HasMore bool `json:"has_more"`
}
type ImDeadLetterViewInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
DeadId string `json:"deadId" dc:"死信ID"`
}
func (in *ImDeadLetterViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择IM租户")
}
if in.DeadId == "" {
return gerror.New("请选择Kafka死信")
}
return nil
}
type ImDeadLetterViewModel struct {
ImDeadLetterListModel
Envelope string `json:"envelope" dc:"Envelope"`
EnvelopeRaw string `json:"envelopeRaw" dc:"Envelope Raw"`
}
type ImDeadLetterViewRaw struct {
ImDeadLetterListModelRaw
Envelope any `json:"envelope"`
EnvelopeRaw string `json:"envelope_raw"`
}
type ImDeadLetterActionInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
DeadId string `json:"deadId" dc:"死信ID"`
ActorUserId int64 `json:"actorUserId" dc:"操作IM用户ID"`
Note string `json:"note" dc:"备注"`
}
func (in *ImDeadLetterActionInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择IM租户")
}
if in.DeadId == "" {
return gerror.New("请选择Kafka死信")
}
if in.ActorUserId <= 0 {
return gerror.New("操作IM用户ID不能为空")
}
return nil
}
@@ -288,3 +288,72 @@ type ImGroupAdminLogListModel struct {
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
type ImGroupCommandModel struct {
CommandID string `json:"commandId" dc:"命令ID"`
EventID string `json:"eventId" dc:"Kafka事件ID"`
Status string `json:"status" dc:"状态"`
TenantID int64 `json:"tenantId" dc:"租户ID"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventType string `json:"eventType" dc:"事件类型"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
type ImGroupProfileUpdateInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
Name string `json:"name" dc:"群名称"`
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
Description string `json:"description" dc:"简介"`
Version int `json:"version" dc:"版本"`
}
type ImGroupGovernanceUpdateInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
JoinApproval *bool `json:"joinApproval" dc:"入群审批"`
MuteAll *bool `json:"muteAll" dc:"全员禁言"`
MemberUploadFiles *bool `json:"memberUploadFiles" dc:"成员上传文件"`
MemberPrivateChat *bool `json:"memberPrivateChat" dc:"成员私聊"`
MemberInvite *bool `json:"memberInvite" dc:"成员邀请"`
Version int `json:"version" dc:"版本"`
}
type ImGroupAdminSetInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
UserIds []int64 `json:"userIds" v:"required#用户ID不能为空" dc:"用户ID列表"`
}
type ImGroupAdminRemoveInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
type ImGroupMemberMuteInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
Reason string `json:"reason" dc:"原因"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
}
type ImGroupMemberUnmuteInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
type ImGroupMemberRemoveInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
type ImGroupOwnerTransferInp struct {
ImGroupTopicInp
ActorUserId int64 `json:"actorUserId" v:"required#操作IM用户ID不能为空" dc:"操作IM用户ID"`
TargetUserId int64 `json:"targetUserId" v:"required#新群主ID不能为空" dc:"新群主ID"`
Version int `json:"version" dc:"版本"`
}
@@ -0,0 +1,65 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImKafkaAuditListInp struct {
form.PageReq
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventId string `json:"eventId" dc:"事件ID"`
EventType string `json:"eventType" dc:"事件类型"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
ActorType string `json:"actorType" dc:"操作者类型"`
ActorId string `json:"actorId" dc:"操作者ID"`
Action string `json:"action" dc:"动作"`
Resource string `json:"resource" dc:"资源"`
ResourceId string `json:"resourceId" dc:"资源ID"`
Result string `json:"result" dc:"结果"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"接收时间"`
}
func (in *ImKafkaAuditListInp) Filter(ctx context.Context) (err error) {
return nil
}
type ImKafkaAuditListModel struct {
Id int64 `json:"id" dc:"ID"`
EventId string `json:"eventId" dc:"事件ID"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventType string `json:"eventType" dc:"事件类型"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
ActorType string `json:"actorType" dc:"操作者类型"`
ActorId string `json:"actorId" dc:"操作者ID"`
Action string `json:"action" dc:"动作"`
Resource string `json:"resource" dc:"资源"`
ResourceId string `json:"resourceId" dc:"资源ID"`
Result string `json:"result" dc:"结果"`
Reason string `json:"reason" dc:"原因"`
Payload string `json:"payload" dc:"Payload"`
Envelope string `json:"envelope" dc:"Envelope"`
OccurredAt *gtime.Time `json:"occurredAt" dc:"发生时间"`
ReceivedAt *gtime.Time `json:"receivedAt" dc:"接收时间"`
}
type ImKafkaAuditViewInp struct {
Id int64 `json:"id" dc:"ID"`
EventId string `json:"eventId" dc:"事件ID"`
}
func (in *ImKafkaAuditViewInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 && in.EventId == "" {
return gerror.New("请选择IM Kafka审计事件")
}
return nil
}
type ImKafkaAuditViewModel struct {
ImKafkaAuditListModel
}
@@ -0,0 +1,126 @@
// Package sysin
// @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 sysin
import (
"context"
"hotgo/internal/model/input/form"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImKafkaCommandListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
CommandId string `json:"commandId" dc:"命令ID"`
EventId string `json:"eventId" dc:"事件ID"`
EventType string `json:"eventType" dc:"事件类型"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
Status string `json:"status" dc:"状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *ImKafkaCommandListInp) Filter(ctx context.Context) (err error) {
return nil
}
type ImKafkaCommandListModel struct {
Id int64 `json:"id" dc:"ID"`
CommandId string `json:"commandId" dc:"命令ID"`
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventId string `json:"eventId" dc:"事件ID"`
EventType string `json:"eventType" dc:"事件类型"`
BizType string `json:"bizType" dc:"业务类型"`
BizId string `json:"bizId" dc:"业务ID"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
Status string `json:"status" dc:"状态"`
Reason string `json:"reason" dc:"错误码"`
Message string `json:"message" dc:"消息"`
SentAt *gtime.Time `json:"sentAt" dc:"投递时间"`
CompletedAt *gtime.Time `json:"completedAt" dc:"完成时间"`
CreatedBy int64 `json:"createdBy" dc:"创建人"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImKafkaCommandViewInp struct {
Id int64 `json:"id" dc:"ID"`
CommandId string `json:"commandId" dc:"命令ID"`
}
func (in *ImKafkaCommandViewInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 && in.CommandId == "" {
return gerror.New("请选择IM后台命令")
}
return nil
}
type ImKafkaCommandViewModel struct {
ImKafkaCommandListModel
Payload string `json:"payload" dc:"Payload"`
Envelope string `json:"envelope" dc:"Envelope"`
Result string `json:"result" dc:"Result"`
}
type ImKafkaCacheInvalidateInp struct {
TenantCode string `json:"tenantCode" dc:"租户编码"`
Namespace string `json:"namespace" dc:"缓存命名空间"`
Keys []string `json:"keys" dc:"缓存Key"`
Reason string `json:"reason" dc:"原因"`
}
func (in *ImKafkaCacheInvalidateInp) Filter(ctx context.Context) (err error) {
in.TenantCode = strings.TrimSpace(in.TenantCode)
in.Namespace = strings.TrimSpace(in.Namespace)
if in.TenantCode == "" {
return gerror.New("请选择租户")
}
if in.Namespace == "" {
return gerror.New("请输入缓存命名空间")
}
keys := make([]string, 0, len(in.Keys))
for _, key := range in.Keys {
if key = strings.TrimSpace(key); key != "" {
keys = append(keys, key)
}
}
in.Keys = keys
return nil
}
type ImKafkaEventModel struct {
EventId string `json:"eventId" dc:"Kafka事件ID"`
}
type ImKafkaWorkbenchEventInp struct {
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventType string `json:"eventType" dc:"事件类型"`
BizId string `json:"bizId" dc:"业务ID"`
Payload map[string]any `json:"payload" dc:"Payload"`
}
func (in *ImKafkaWorkbenchEventInp) Filter(ctx context.Context) (err error) {
in.TenantCode = strings.TrimSpace(in.TenantCode)
in.EventType = strings.TrimSpace(in.EventType)
in.BizId = strings.TrimSpace(in.BizId)
if in.TenantCode == "" {
return gerror.New("请选择租户")
}
if in.EventType == "" {
in.EventType = "workbench.connectivity.test"
}
if in.BizId == "" {
in.BizId = "admin_connectivity_test"
}
if in.Payload == nil {
in.Payload = map[string]any{}
}
return nil
}
@@ -78,3 +78,45 @@ type ImUserViewModel struct {
AccessJson string `json:"accessJson" dc:"访问权限"`
ProfileVersion int `json:"profileVersion" dc:"资料版本"`
}
type ImUserCommandModel struct {
CommandID string `json:"commandId" dc:"命令ID"`
EventID string `json:"eventId" dc:"事件ID"`
Status string `json:"status" dc:"状态"`
TenantID int64 `json:"tenantId" dc:"租户ID"`
TenantCode string `json:"tenantCode" dc:"租户编码"`
EventType string `json:"eventType" dc:"事件类型"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
type ImUserGovernanceInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId int64 `json:"userId" dc:"目标用户ID"`
ActorUserId int64 `json:"actorUserId" dc:"操作IM用户ID"`
DeviceId string `json:"deviceId" dc:"设备ID"`
Reason string `json:"reason" dc:"原因"`
Hard bool `json:"hard" dc:"硬删除"`
}
func (in *ImUserGovernanceInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.UserId <= 0 {
return gerror.New("用户ID不能为空")
}
if in.ActorUserId <= 0 {
return gerror.New("操作IM用户ID不能为空")
}
return nil
}
func (in *ImUserGovernanceInp) FilterDevice(ctx context.Context) (err error) {
if err = in.Filter(ctx); err != nil {
return err
}
if in.DeviceId == "" {
return gerror.New("设备ID不能为空")
}
return nil
}
@@ -0,0 +1,52 @@
// Package queues
// @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 queues
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/imkafka"
"hotgo/internal/library/queue"
)
func init() {
queue.RegisterConsumer(ImAdminCommandResult)
}
var ImAdminCommandResult = &qImAdminCommandResult{}
type qImAdminCommandResult struct{}
func (q *qImAdminCommandResult) GetTopic() string {
return consts.QueueImAdminCommandResultTopic
}
func (q *qImAdminCommandResult) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
var envelope imkafka.Envelope
if err = json.Unmarshal(mqMsg.Body, &envelope); err == nil && envelope.EventID != "" && len(envelope.Payload) > 0 {
var result imkafka.CommandResult
if err = json.Unmarshal(envelope.Payload, &result); err != nil {
return err
}
if result.CommandEventID == "" {
result.CommandEventID = envelope.ParentEventID
}
if result.TenantCode == "" {
result.TenantCode = envelope.TenantCode
}
if result.EventID == "" {
result.EventID = envelope.EventID
}
return imkafka.ApplyCommandResult(ctx, result)
}
var result imkafka.CommandResult
if err = json.Unmarshal(mqMsg.Body, &result); err != nil {
return err
}
return imkafka.ApplyCommandResult(ctx, result)
}
+54
View File
@@ -0,0 +1,54 @@
package queues
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/imkafka"
"hotgo/internal/library/queue"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
func init() {
queue.RegisterConsumer(ImAuditEvent)
}
var ImAuditEvent = &qImAuditEvent{}
type qImAuditEvent struct{}
func (q *qImAuditEvent) GetTopic() string {
return consts.QueueImAuditEventTopic
}
func (q *qImAuditEvent) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
var envelope imkafka.Envelope
if err = json.Unmarshal(mqMsg.Body, &envelope); err != nil {
return err
}
payload := map[string]any{}
if len(envelope.Payload) > 0 {
_ = json.Unmarshal(envelope.Payload, &payload)
}
_, err = g.DB().Model("im_audit_events").Safe().Ctx(ctx).Data(g.Map{
"event_id": envelope.EventID,
"tenant_code": envelope.TenantCode,
"event_type": envelope.EventType,
"biz_type": envelope.BizType,
"biz_id": envelope.BizID,
"actor_type": firstString(payload["actor_type"]),
"actor_id": firstString(payload["actor_id"]),
"action": firstString(payload["action"]),
"resource": firstString(payload["resource"]),
"resource_id": firstString(payload["resource_id"]),
"result": firstString(payload["result"]),
"reason": firstString(payload["reason"]),
"payload": string(envelope.Payload),
"envelope": string(mqMsg.Body),
"occurred_at": envelope.OccurredAt,
"received_at": gtime.Now(),
}).InsertIgnore()
return err
}
+74
View File
@@ -0,0 +1,74 @@
package queues
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/imkafka"
"hotgo/internal/library/queue"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
func init() {
queue.RegisterConsumer(ImDeadLetter)
}
var ImDeadLetter = &qImDeadLetter{}
type qImDeadLetter struct{}
func (q *qImDeadLetter) GetTopic() string {
return consts.QueueImDeadLetterTopic
}
func (q *qImDeadLetter) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
var envelope imkafka.Envelope
if err = json.Unmarshal(mqMsg.Body, &envelope); err != nil {
return err
}
payload := map[string]any{}
if len(envelope.Payload) > 0 {
_ = json.Unmarshal(envelope.Payload, &payload)
}
_, err = g.DB().Model("im_kafka_dead_letter_events").Safe().Ctx(ctx).Data(g.Map{
"event_id": envelope.EventID,
"parent_event_id": envelope.ParentEventID,
"tenant_code": envelope.TenantCode,
"event_type": envelope.EventType,
"biz_type": envelope.BizType,
"biz_id": envelope.BizID,
"source_service": envelope.SourceService,
"target_service": envelope.TargetService,
"dead_id": firstString(payload["dead_id"], payload["deadId"], payload["DeadID"]),
"reason": firstString(payload["reason"], payload["Reason"], payload["last_reason"], payload["lastReason"], payload["LastReason"]),
"message": firstString(payload["message"], payload["Message"], payload["last_error"], payload["lastError"], payload["LastError"]),
"payload": string(envelope.Payload),
"envelope": string(mqMsg.Body),
"received_at": gtime.Now(),
}).InsertIgnore()
return err
}
func stringValue(v any) string {
if v == nil {
return ""
}
switch value := v.(type) {
case string:
return value
default:
b, _ := json.Marshal(value)
return string(b)
}
}
func firstString(values ...any) string {
for _, v := range values {
if text := stringValue(v); text != "" && text != "null" {
return text
}
}
return ""
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImDeadLetter) // IM Kafka死信
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImKafkaAudit) // IM Kafka审计事件
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImKafkaCommand) // IM后台命令
}
+18
View File
@@ -0,0 +1,18 @@
package router
import (
"context"
"hotgo/internal/controller/internalapi/imconfig"
"hotgo/internal/library/iminternal"
"github.com/gogf/gf/v2/net/ghttp"
)
func Internal(ctx context.Context, group *ghttp.RouterGroup) {
group.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(iminternal.Middleware)
group.Bind(
imconfig.Config,
)
})
}
+96
View File
@@ -210,6 +210,22 @@ type (
List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error)
// View 获取IM用户指定信息
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
// Ban 通过Kafka封禁IM用户
Ban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// Unban 通过Kafka解封IM用户
Unban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// Freeze 通过Kafka冻结IM用户
Freeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// Unfreeze 通过Kafka解冻IM用户
Unfreeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// DeviceKick 通过Kafka踢出IM用户设备
DeviceKick(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// LogoutAll 通过Kafka踢出IM用户全部在线会话
LogoutAll(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// AccountCancel 通过Kafka注销IM用户账号
AccountCancel(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
// AccountDelete 通过Kafka删除IM用户账号
AccountDelete(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error)
}
ISysImGroup interface {
// List 获取IM群组列表
@@ -230,6 +246,22 @@ type (
MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error)
// AdminLogList 获取IM群管理日志列表
AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error)
// ProfileUpdate 通过Kafka修改IM群资料
ProfileUpdate(ctx context.Context, in *sysin.ImGroupProfileUpdateInp) (res *sysin.ImGroupCommandModel, err error)
// GovernanceUpdate 通过Kafka修改IM群治理配置
GovernanceUpdate(ctx context.Context, in *sysin.ImGroupGovernanceUpdateInp) (res *sysin.ImGroupCommandModel, err error)
// AdminSet 通过Kafka设置IM群管理员
AdminSet(ctx context.Context, in *sysin.ImGroupAdminSetInp) (res *sysin.ImGroupCommandModel, err error)
// AdminRemove 通过Kafka删除IM群管理员
AdminRemove(ctx context.Context, in *sysin.ImGroupAdminRemoveInp) (res *sysin.ImGroupCommandModel, err error)
// MemberMute 通过Kafka禁言IM群成员
MemberMute(ctx context.Context, in *sysin.ImGroupMemberMuteInp) (res *sysin.ImGroupCommandModel, err error)
// MemberUnmute 通过Kafka解除IM群成员禁言
MemberUnmute(ctx context.Context, in *sysin.ImGroupMemberUnmuteInp) (res *sysin.ImGroupCommandModel, err error)
// MemberRemove 通过Kafka移出IM群成员
MemberRemove(ctx context.Context, in *sysin.ImGroupMemberRemoveInp) (res *sysin.ImGroupCommandModel, err error)
// OwnerTransfer 通过Kafka转让IM群主
OwnerTransfer(ctx context.Context, in *sysin.ImGroupOwnerTransferInp) (res *sysin.ImGroupCommandModel, err error)
}
ISysImRealname interface {
// List 获取IM实名审核列表
@@ -253,6 +285,34 @@ type (
// View 获取IM审计日志指定信息
View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error)
}
ISysImKafkaAudit interface {
// List 获取IM Kafka审计事件列表
List(ctx context.Context, in *sysin.ImKafkaAuditListInp) (list []*sysin.ImKafkaAuditListModel, totalCount int, err error)
// View 获取IM Kafka审计事件详情
View(ctx context.Context, in *sysin.ImKafkaAuditViewInp) (res *sysin.ImKafkaAuditViewModel, err error)
}
ISysImKafkaCommand interface {
// List 获取IM后台命令列表
List(ctx context.Context, in *sysin.ImKafkaCommandListInp) (list []*sysin.ImKafkaCommandListModel, totalCount int, err error)
// View 获取IM后台命令详情
View(ctx context.Context, in *sysin.ImKafkaCommandViewInp) (res *sysin.ImKafkaCommandViewModel, err error)
// CacheInvalidate 发送IM缓存失效测试事件
CacheInvalidate(ctx context.Context, in *sysin.ImKafkaCacheInvalidateInp) (res *sysin.ImKafkaEventModel, err error)
// WorkbenchEvent 发送IM工作台测试事件
WorkbenchEvent(ctx context.Context, in *sysin.ImKafkaWorkbenchEventInp) (res *sysin.ImKafkaEventModel, err error)
}
ISysImDeadLetter interface {
// List 获取IM Kafka死信列表
List(ctx context.Context, in *sysin.ImDeadLetterListInp) (res *sysin.ImDeadLetterPageModel, err error)
// View 获取IM Kafka死信详情
View(ctx context.Context, in *sysin.ImDeadLetterViewInp) (res *sysin.ImDeadLetterViewModel, err error)
// Replay 重放IM Kafka死信
Replay(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error)
// Ignore 忽略IM Kafka死信
Ignore(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error)
// Resolve 标记IM Kafka死信已处理
Resolve(ctx context.Context, in *sysin.ImDeadLetterActionInp) (res *sysin.ImGroupCommandModel, err error)
}
ISysDictData interface {
// Delete 删除
Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error
@@ -504,6 +564,9 @@ var (
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImAuditLog ISysImAuditLog
localSysImKafkaAudit ISysImKafkaAudit
localSysImKafkaCommand ISysImKafkaCommand
localSysImDeadLetter ISysImDeadLetter
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
@@ -695,6 +758,39 @@ func RegisterSysImAuditLog(i ISysImAuditLog) {
localSysImAuditLog = i
}
func SysImKafkaAudit() ISysImKafkaAudit {
if localSysImKafkaAudit == nil {
panic("implement not found for interface ISysImKafkaAudit, forgot register?")
}
return localSysImKafkaAudit
}
func RegisterSysImKafkaAudit(i ISysImKafkaAudit) {
localSysImKafkaAudit = i
}
func SysImKafkaCommand() ISysImKafkaCommand {
if localSysImKafkaCommand == nil {
panic("implement not found for interface ISysImKafkaCommand, forgot register?")
}
return localSysImKafkaCommand
}
func RegisterSysImKafkaCommand(i ISysImKafkaCommand) {
localSysImKafkaCommand = i
}
func SysImDeadLetter() ISysImDeadLetter {
if localSysImDeadLetter == nil {
panic("implement not found for interface ISysImDeadLetter, forgot register?")
}
return localSysImDeadLetter
}
func RegisterSysImDeadLetter(i ISysImDeadLetter) {
localSysImDeadLetter = i
}
func SysDictData() ISysDictData {
if localSysDictData == nil {
panic("implement not found for interface ISysDictData, forgot register?")
+17 -1
View File
@@ -212,10 +212,16 @@ queue:
retry: 0 # 重试次数
logLevel: "info" # rocketmq日志输出级别,可选:all|close|debug|info|warn|error|fatal
kafka:
address: "127.0.0.1:9092" # kafka地址+端口
address: ["127.0.0.1:9092"] # kafka地址+端口,支持多个
version: "2.0.0.0" # kafka专属配置,默认2.0.0.0
randClient: true # 开启随机生成clientID,可以实现启动多实例同时一起消费相同topic,加速消费能力的特性,默认为true
multiConsumer: true # 是否支持创建多个消费者
clientId: "hotgo-admin" # Kafka客户端ID
userName: "${KAFKA_USERNAME}" # SASL用户名,未启用SASL可留空
password: "${KAFKA_PASSWORD}" # SASL密码,未启用SASL可留空
saslEnabled: false # 是否开启SASL
saslMechanism: "SCRAM-SHA-512" # SASL机制,可选:PLAIN|SCRAM-SHA-256|SCRAM-SHA-512
tlsEnabled: false # 是否开启TLS
# Redis. 配置参考:https://goframe.org/pages/viewpage.action?pageId=1114217
@@ -310,3 +316,13 @@ imAdminApi:
serviceId: "hotgo-admin"
serviceSecret: "${IM_ADMIN_API_SECRET}"
timeout: "5s"
imInternalApi:
baseUrl: "http://127.0.0.1:6060/v1"
serviceName: "hotgo-admin"
serviceSecret: "${IM_INTERNAL_API_SECRET}"
imConfigApi:
serviceName: "im-server"
serviceSecret: "${IM_CONFIG_API_SECRET}"
timeWindow: "300s"
@@ -0,0 +1,35 @@
SET NAMES utf8mb4;
SET @now = NOW();
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now
WHERE @imRootId IS NULL;
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
SELECT @deadLetterId := `id` FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, 'Kafka死信', 'imDeadLetterIndex', 'dead-letter', '', '2', '', '/imDeadLetter/list', '', '/imDeadLetter/index', '1', 'imDeadLetterIndex', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '91', '', '1', @now, @now
WHERE @deadLetterId IS NULL;
SELECT @deadLetterId := `id` FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @deadLetterId, '死信详情', 'imDeadLetterView', '', '', '3', '', '/imDeadLetter/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @deadLetterId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterView');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @deadLetterId, '重放死信', 'imDeadLetterReplay', '', '', '3', '', '/imDeadLetter/replay', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @deadLetterId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterReplay');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @deadLetterId, '忽略死信', 'imDeadLetterIgnore', '', '', '3', '', '/imDeadLetter/ignore', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @deadLetterId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterIgnore');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @deadLetterId, '标记已处理', 'imDeadLetterResolve', '', '', '3', '', '/imDeadLetter/resolve', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @deadLetterId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imDeadLetterResolve');
@@ -57,3 +57,35 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMuteList'
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '群管理日志', 'imGroupAdminLogList', '', '', '3', '', '/imGroup/adminLogList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '80', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAdminLogList');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '修改群资料', 'imGroupProfileUpdate', '', '', '3', '', '/imGroup/profileUpdate', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '90', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupProfileUpdate');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '修改群治理配置', 'imGroupGovernanceUpdate', '', '', '3', '', '/imGroup/governanceUpdate', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '100', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupGovernanceUpdate');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '设置群管理员', 'imGroupAdminSet', '', '', '3', '', '/imGroup/adminSet', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '110', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAdminSet');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '删除群管理员', 'imGroupAdminRemove', '', '', '3', '', '/imGroup/adminRemove', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '120', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAdminRemove');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '禁言群成员', 'imGroupMemberMute', '', '', '3', '', '/imGroup/memberMute', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '130', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberMute');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '解除群成员禁言', 'imGroupMemberUnmute', '', '', '3', '', '/imGroup/memberUnmute', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '140', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberUnmute');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '移出群成员', 'imGroupMemberRemove', '', '', '3', '', '/imGroup/memberRemove', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '150', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberRemove');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @groupListId, '转让群主', 'imGroupOwnerTransfer', '', '', '3', '', '/imGroup/ownerTransfer', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '160', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupOwnerTransfer');
@@ -0,0 +1,57 @@
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `im_admin_commands` (
`id` bigint NOT NULL AUTO_INCREMENT,
`command_id` varchar(96) NOT NULL COMMENT '命令ID',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT 'IM租户ID',
`tenant_code` varchar(64) NOT NULL DEFAULT '' COMMENT 'IM租户编码',
`event_id` varchar(96) NOT NULL COMMENT 'Kafka事件ID',
`event_type` varchar(96) NOT NULL DEFAULT '' COMMENT '事件类型',
`biz_type` varchar(64) NOT NULL DEFAULT '' COMMENT '业务类型',
`biz_id` varchar(128) NOT NULL DEFAULT '' COMMENT '业务ID',
`idempotency_key` varchar(128) NOT NULL DEFAULT '' COMMENT '幂等键',
`status` varchar(24) NOT NULL DEFAULT 'pending' COMMENT '状态',
`payload` json NOT NULL COMMENT '命令Payload',
`envelope` json NOT NULL COMMENT 'Kafka Envelope',
`result` json DEFAULT NULL COMMENT '执行结果',
`reason` varchar(96) NOT NULL DEFAULT '' COMMENT '稳定错误码',
`message` varchar(1000) NOT NULL DEFAULT '' COMMENT '结果消息',
`sent_at` datetime(3) DEFAULT NULL COMMENT '投递时间',
`completed_at` datetime(3) DEFAULT NULL COMMENT '完成时间',
`created_by` bigint NOT NULL DEFAULT 0 COMMENT '创建人',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_admin_commands_command` (`command_id`),
UNIQUE KEY `uk_im_admin_commands_event` (`event_id`),
KEY `idx_im_admin_commands_tenant_status` (`tenant_id`, `status`, `id`),
KEY `idx_im_admin_commands_biz` (`biz_type`, `biz_id`),
KEY `idx_im_admin_commands_type` (`event_type`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM后台Kafka命令';
CREATE TABLE IF NOT EXISTS `im_admin_outbox` (
`id` bigint NOT NULL AUTO_INCREMENT,
`event_id` varchar(96) NOT NULL COMMENT 'Kafka事件ID',
`command_id` varchar(96) NOT NULL DEFAULT '' COMMENT '命令ID',
`topic` varchar(128) NOT NULL COMMENT 'Kafka Topic',
`event_type` varchar(96) NOT NULL DEFAULT '' COMMENT '事件类型',
`biz_type` varchar(64) NOT NULL DEFAULT '' COMMENT '业务类型',
`biz_id` varchar(128) NOT NULL DEFAULT '' COMMENT '业务ID',
`key_value` varchar(256) NOT NULL DEFAULT '' COMMENT 'Kafka Key',
`envelope` json NOT NULL COMMENT 'Kafka Envelope',
`status` varchar(24) NOT NULL DEFAULT 'pending' COMMENT '状态',
`attempts` int NOT NULL DEFAULT 0 COMMENT '投递次数',
`next_retry_at` datetime(3) DEFAULT NULL COMMENT '下次重试时间',
`sent_at` datetime(3) DEFAULT NULL COMMENT '投递时间',
`last_reason` varchar(96) NOT NULL DEFAULT '' COMMENT '最近失败码',
`last_error` varchar(1000) NOT NULL DEFAULT '' COMMENT '最近失败',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_admin_outbox_event` (`event_id`),
KEY `idx_im_admin_outbox_pending` (`status`, `next_retry_at`, `id`),
KEY `idx_im_admin_outbox_command` (`command_id`),
KEY `idx_im_admin_outbox_biz` (`biz_type`, `biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM后台Kafka Outbox';
UPDATE `im_admin_commands` SET `status` = 'running' WHERE `status` = 'sent';
@@ -0,0 +1,23 @@
SET NAMES utf8mb4;
SET @now = NOW();
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now
WHERE @imRootId IS NULL;
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
SELECT @auditId := `id` FROM `hg_admin_menu` WHERE `name` = 'imKafkaAuditIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, 'Kafka审计事件', 'imKafkaAuditIndex', 'kafka-audit', '', '2', '', '/imKafkaAudit/list', '', '/imKafkaAudit/index', '1', 'imKafkaAuditIndex', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '95', '', '1', @now, @now
WHERE @auditId IS NULL;
SELECT @auditId := `id` FROM `hg_admin_menu` WHERE `name` = 'imKafkaAuditIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @auditId, '审计事件详情', 'imKafkaAuditView', '', '', '3', '', '/imKafkaAudit/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @auditId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imKafkaAuditView');
@@ -0,0 +1,31 @@
SET NAMES utf8mb4;
SET @now = NOW();
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now
WHERE @imRootId IS NULL;
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
SELECT @commandId := `id` FROM `hg_admin_menu` WHERE `name` = 'imKafkaCommandIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, 'Kafka命令', 'imKafkaCommandIndex', 'kafka-command', '', '2', '', '/imKafkaCommand/list', '', '/imKafkaCommand/index', '1', 'imKafkaCommandIndex', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '90', '', '1', @now, @now
WHERE @commandId IS NULL;
SELECT @commandId := `id` FROM `hg_admin_menu` WHERE `name` = 'imKafkaCommandIndex' LIMIT 1;
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @commandId, '命令详情', 'imKafkaCommandView', '', '', '3', '', '/imKafkaCommand/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @commandId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imKafkaCommandView');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @commandId, '发送缓存失效测试', 'imKafkaCommandCacheInvalidate', '', '', '3', '', '/imKafkaCommand/cacheInvalidate', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @commandId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imKafkaCommandCacheInvalidate');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @commandId, '发送工作台测试事件', 'imKafkaCommandWorkbenchEvent', '', '', '3', '', '/imKafkaCommand/workbenchEvent', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @commandId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imKafkaCommandWorkbenchEvent');
@@ -0,0 +1,50 @@
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `im_kafka_dead_letter_events` (
`id` bigint NOT NULL AUTO_INCREMENT,
`event_id` varchar(96) NOT NULL COMMENT '事件ID',
`parent_event_id` varchar(96) NOT NULL DEFAULT '' COMMENT '父事件ID',
`tenant_code` varchar(64) NOT NULL DEFAULT '' COMMENT '租户编码',
`event_type` varchar(96) NOT NULL DEFAULT '' COMMENT '事件类型',
`biz_type` varchar(64) NOT NULL DEFAULT '' COMMENT '业务类型',
`biz_id` varchar(128) NOT NULL DEFAULT '' COMMENT '业务ID',
`source_service` varchar(64) NOT NULL DEFAULT '' COMMENT '来源服务',
`target_service` varchar(64) NOT NULL DEFAULT '' COMMENT '目标服务',
`dead_id` varchar(128) NOT NULL DEFAULT '' COMMENT '死信ID',
`reason` varchar(128) NOT NULL DEFAULT '' COMMENT '原因码',
`message` varchar(1000) NOT NULL DEFAULT '' COMMENT '错误信息',
`payload` json DEFAULT NULL COMMENT 'Payload',
`envelope` json NOT NULL COMMENT 'Envelope',
`received_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '接收时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_kafka_dead_letter_events_event` (`event_id`),
KEY `idx_im_kafka_dead_letter_events_tenant` (`tenant_code`, `id`),
KEY `idx_im_kafka_dead_letter_events_dead` (`dead_id`),
KEY `idx_im_kafka_dead_letter_events_biz` (`biz_type`, `biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM Kafka死信事件';
CREATE TABLE IF NOT EXISTS `im_audit_events` (
`id` bigint NOT NULL AUTO_INCREMENT,
`event_id` varchar(96) NOT NULL COMMENT '事件ID',
`tenant_code` varchar(64) NOT NULL DEFAULT '' COMMENT '租户编码',
`event_type` varchar(96) NOT NULL DEFAULT '' COMMENT '事件类型',
`biz_type` varchar(64) NOT NULL DEFAULT '' COMMENT '业务类型',
`biz_id` varchar(128) NOT NULL DEFAULT '' COMMENT '业务ID',
`actor_type` varchar(32) NOT NULL DEFAULT '' COMMENT '操作者类型',
`actor_id` varchar(64) NOT NULL DEFAULT '' COMMENT '操作者ID',
`action` varchar(128) NOT NULL DEFAULT '' COMMENT '动作',
`resource` varchar(64) NOT NULL DEFAULT '' COMMENT '资源',
`resource_id` varchar(128) NOT NULL DEFAULT '' COMMENT '资源ID',
`result` varchar(32) NOT NULL DEFAULT '' COMMENT '结果',
`reason` varchar(128) NOT NULL DEFAULT '' COMMENT '原因',
`payload` json DEFAULT NULL COMMENT 'Payload',
`envelope` json NOT NULL COMMENT 'Envelope',
`occurred_at` datetime(3) DEFAULT NULL COMMENT '发生时间',
`received_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '接收时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_audit_events_event` (`event_id`),
KEY `idx_im_audit_events_tenant` (`tenant_code`, `id`),
KEY `idx_im_audit_events_actor` (`actor_type`, `actor_id`),
KEY `idx_im_audit_events_resource` (`resource`, `resource_id`),
KEY `idx_im_audit_events_action` (`action`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM Kafka审计事件';
@@ -29,3 +29,35 @@ SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' LIM
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUser/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '封禁用户', 'imUserBan', '', '', '3', '', '/imUser/ban', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserBan');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '解封用户', 'imUserUnban', '', '', '3', '', '/imUser/unban', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserUnban');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '冻结用户', 'imUserFreeze', '', '', '3', '', '/imUser/freeze', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserFreeze');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '解冻用户', 'imUserUnfreeze', '', '', '3', '', '/imUser/unfreeze', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserUnfreeze');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '踢出设备', 'imUserDeviceKick', '', '', '3', '', '/imUser/deviceKick', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '60', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserDeviceKick');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '踢出全部会话', 'imUserLogoutAll', '', '', '3', '', '/imUser/logoutAll', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '70', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserLogoutAll');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '注销账号', 'imUserAccountCancel', '', '', '3', '', '/imUser/accountCancel', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '80', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserAccountCancel');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '删除账号', 'imUserAccountDelete', '', '', '3', '', '/imUser/accountDelete', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '90', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserAccountDelete');