feat: add im kafka admin integration
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -62,6 +62,16 @@ type GovernanceUpdateReq struct {
|
||||
|
||||
type GovernanceUpdateRes struct{}
|
||||
|
||||
// ProfileUpdateReq 修改IM群资料
|
||||
type ProfileUpdateReq struct {
|
||||
g.Meta `path:"/imGroup/profileUpdate" method:"post" tags:"IM群组" summary:"修改IM群资料"`
|
||||
sysin.ImGroupProfileUpdateInp
|
||||
}
|
||||
|
||||
type ProfileUpdateRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// DissolveReq 解散IM群
|
||||
type DissolveReq struct {
|
||||
g.Meta `path:"/imGroup/dissolve" method:"post" tags:"IM群组" summary:"解散IM群"`
|
||||
@@ -78,6 +88,16 @@ type MuteReq struct {
|
||||
|
||||
type MuteRes struct{}
|
||||
|
||||
// MemberMuteReq Kafka禁言IM群成员
|
||||
type MemberMuteReq struct {
|
||||
g.Meta `path:"/imGroup/memberMute" method:"post" tags:"IM群组" summary:"Kafka禁言IM群成员"`
|
||||
sysin.ImGroupMuteInp
|
||||
}
|
||||
|
||||
type MemberMuteRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// UnmuteReq 解除IM群成员禁言
|
||||
type UnmuteReq struct {
|
||||
g.Meta `path:"/imGroup/unmute" method:"post" tags:"IM群组" summary:"解除IM群成员禁言"`
|
||||
@@ -86,6 +106,26 @@ type UnmuteReq struct {
|
||||
|
||||
type UnmuteRes struct{}
|
||||
|
||||
// MemberUnmuteReq Kafka解除IM群成员禁言
|
||||
type MemberUnmuteReq struct {
|
||||
g.Meta `path:"/imGroup/memberUnmute" method:"post" tags:"IM群组" summary:"Kafka解除IM群成员禁言"`
|
||||
sysin.ImGroupUnmuteInp
|
||||
}
|
||||
|
||||
type MemberUnmuteRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// MemberRemoveReq 移出IM群成员
|
||||
type MemberRemoveReq struct {
|
||||
g.Meta `path:"/imGroup/memberRemove" method:"post" tags:"IM群组" summary:"移出IM群成员"`
|
||||
sysin.ImGroupMemberRemoveInp
|
||||
}
|
||||
|
||||
type MemberRemoveRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// JoinRequestApproveReq 通过IM入群申请
|
||||
type JoinRequestApproveReq struct {
|
||||
g.Meta `path:"/imGroup/joinRequestApprove" method:"post" tags:"IM群组" summary:"通过IM入群申请"`
|
||||
@@ -110,6 +150,26 @@ type RoleUpdateReq struct {
|
||||
|
||||
type RoleUpdateRes struct{}
|
||||
|
||||
// AdminSetReq 设置IM群管理员
|
||||
type AdminSetReq struct {
|
||||
g.Meta `path:"/imGroup/adminSet" method:"post" tags:"IM群组" summary:"设置IM群管理员"`
|
||||
sysin.ImGroupRoleUpdateInp
|
||||
}
|
||||
|
||||
type AdminSetRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// AdminRemoveReq 删除IM群管理员
|
||||
type AdminRemoveReq struct {
|
||||
g.Meta `path:"/imGroup/adminRemove" method:"post" tags:"IM群组" summary:"删除IM群管理员"`
|
||||
sysin.ImGroupRoleUpdateInp
|
||||
}
|
||||
|
||||
type AdminRemoveRes struct {
|
||||
*sysin.ImGroupCommandModel
|
||||
}
|
||||
|
||||
// OwnerTransferReq 转让IM群主
|
||||
type OwnerTransferReq struct {
|
||||
g.Meta `path:"/imGroup/ownerTransfer" method:"post" tags:"IM群组" summary:"转让IM群主"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -63,3 +63,75 @@ type DevicesRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImUserDeviceListModel `json:"list" dc:"设备列表"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ var (
|
||||
}...)
|
||||
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
// 注册内部服务路由
|
||||
router.Internal(ctx, group)
|
||||
|
||||
// 注册后台路由
|
||||
router.Admin(ctx, group)
|
||||
|
||||
|
||||
@@ -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, "已处理"),
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -81,6 +81,17 @@ func (c *cImGroup) GovernanceUpdate(ctx context.Context, req *imgroup.Governance
|
||||
return
|
||||
}
|
||||
|
||||
// ProfileUpdate 修改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
|
||||
}
|
||||
|
||||
// Dissolve 解散IM群
|
||||
func (c *cImGroup) Dissolve(ctx context.Context, req *imgroup.DissolveReq) (res *imgroup.DissolveRes, err error) {
|
||||
err = service.SysImGroup().Dissolve(ctx, &req.ImGroupDissolveInp)
|
||||
@@ -93,12 +104,45 @@ func (c *cImGroup) Mute(ctx context.Context, req *imgroup.MuteReq) (res *imgroup
|
||||
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.ImGroupMuteInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.MemberMuteRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// Unmute 解除IM群成员禁言
|
||||
func (c *cImGroup) Unmute(ctx context.Context, req *imgroup.UnmuteReq) (res *imgroup.UnmuteRes, err error) {
|
||||
err = service.SysImGroup().Unmute(ctx, &req.ImGroupUnmuteInp)
|
||||
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.ImGroupUnmuteInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.MemberUnmuteRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// MemberRemove 移出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
|
||||
}
|
||||
|
||||
// JoinRequestApprove 通过IM入群申请
|
||||
func (c *cImGroup) JoinRequestApprove(ctx context.Context, req *imgroup.JoinRequestApproveReq) (res *imgroup.JoinRequestApproveRes, err error) {
|
||||
err = service.SysImGroup().JoinRequestApprove(ctx, &req.ImGroupJoinRequestResolveInp)
|
||||
@@ -117,6 +161,28 @@ func (c *cImGroup) RoleUpdate(ctx context.Context, req *imgroup.RoleUpdateReq) (
|
||||
return
|
||||
}
|
||||
|
||||
// AdminSet 设置IM群管理员
|
||||
func (c *cImGroup) AdminSet(ctx context.Context, req *imgroup.AdminSetReq) (res *imgroup.AdminSetRes, err error) {
|
||||
data, err := service.SysImGroup().AdminSet(ctx, &req.ImGroupRoleUpdateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.AdminSetRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// AdminRemove 删除IM群管理员
|
||||
func (c *cImGroup) AdminRemove(ctx context.Context, req *imgroup.AdminRemoveReq) (res *imgroup.AdminRemoveRes, err error) {
|
||||
data, err := service.SysImGroup().AdminRemove(ctx, &req.ImGroupRoleUpdateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.AdminRemoveRes)
|
||||
res.ImGroupCommandModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// OwnerTransfer 转让IM群主
|
||||
func (c *cImGroup) OwnerTransfer(ctx context.Context, req *imgroup.OwnerTransferReq) (res *imgroup.OwnerTransferRes, err error) {
|
||||
err = service.SysImGroup().OwnerTransfer(ctx, &req.ImGroupOwnerTransferInp)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -77,3 +77,83 @@ func (c *cImUser) Devices(ctx context.Context, req *imuser.DevicesReq) (res *imu
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
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,143 @@
|
||||
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/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().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 *entity.SysConfig
|
||||
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.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().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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package imadmin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
)
|
||||
|
||||
const maxResponseBody = 10 << 20
|
||||
|
||||
type AdminScope struct {
|
||||
TenantId int64
|
||||
OperatorId int64
|
||||
RoleKey string
|
||||
RequestId string
|
||||
}
|
||||
|
||||
type Page[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
conf Config
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func NewClient(conf Config) *Client {
|
||||
return &Client{
|
||||
conf: conf,
|
||||
http: &http.Client{Timeout: conf.Timeout},
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultClient(ctx context.Context) *Client {
|
||||
conf := LoadConfig(ctx)
|
||||
return NewClient(conf)
|
||||
}
|
||||
|
||||
func (c *Client) CreateCode(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes", scope, in, out)
|
||||
}
|
||||
|
||||
func (c *Client) CreateBatch(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-code-batches", scope, in, out)
|
||||
}
|
||||
|
||||
func (c *Client) ListCodes(ctx context.Context, scope AdminScope, in any, out any) error {
|
||||
if hasInviteCode(in) {
|
||||
return c.doJSON(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", scope, in, out)
|
||||
}
|
||||
path := "/admin/v1/im/registration-invite-codes"
|
||||
if query := toQuery(in); query != "" {
|
||||
path += "?" + query
|
||||
}
|
||||
return c.doJSON(ctx, http.MethodGet, path, scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) GetBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error {
|
||||
return c.doJSON(ctx, http.MethodGet, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d", batchId), scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) RevokeCode(ctx context.Context, scope AdminScope, codeId int64) error {
|
||||
return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-codes/%d/revoke", codeId), scope, nil, nil)
|
||||
}
|
||||
|
||||
func (c *Client) RevokeBatch(ctx context.Context, scope AdminScope, batchId int64, out any) error {
|
||||
return c.doJSON(ctx, http.MethodPost, fmt.Sprintf("/admin/v1/im/registration-invite-code-batches/%d/revoke", batchId), scope, nil, out)
|
||||
}
|
||||
|
||||
func (c *Client) ExportCodes(ctx context.Context, scope AdminScope, in any) (*http.Response, error) {
|
||||
req, err := c.newRequest(ctx, http.MethodPost, "/admin/v1/im/registration-invite-codes/export", scope, in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, gerror.New("IM服务暂不可用")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
defer resp.Body.Close()
|
||||
return nil, c.parseError(resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method string, path string, scope AdminScope, in any, out any) error {
|
||||
req, err := c.newRequest(ctx, method, path, scope, in)
|
||||
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) newRequest(ctx context.Context, method string, path string, scope AdminScope, in any) (*http.Request, error) {
|
||||
if c.conf.BaseUrl == "" {
|
||||
return nil, gerror.New("IM管理API未配置")
|
||||
}
|
||||
if c.conf.ServiceSecret == "" {
|
||||
return nil, gerror.New("IM管理API密钥未配置")
|
||||
}
|
||||
var body io.Reader
|
||||
bodyBytes := []byte{}
|
||||
if in != nil {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "序列化IM请求失败")
|
||||
}
|
||||
bodyBytes = b
|
||||
body = bytes.NewReader(bodyBytes)
|
||||
}
|
||||
baseUrl := strings.TrimRight(c.conf.BaseUrl, "/")
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseUrl+path, body)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "创建IM请求失败")
|
||||
}
|
||||
if scope.RequestId == "" {
|
||||
scope.RequestId = gctx.CtxId(ctx)
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
nonce, err := randomNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyHash := sha256.Sum256(bodyBytes)
|
||||
canonical := strings.Join([]string{
|
||||
method,
|
||||
requestTarget(req),
|
||||
c.conf.ServiceId,
|
||||
fmt.Sprintf("%d", scope.TenantId),
|
||||
fmt.Sprintf("%d", scope.OperatorId),
|
||||
timestamp,
|
||||
nonce,
|
||||
scope.RequestId,
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(c.conf.ServiceSecret))
|
||||
_, _ = mac.Write([]byte(canonical))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "IM-HMAC "+hex.EncodeToString(mac.Sum(nil)))
|
||||
req.Header.Set("X-IM-Service-ID", c.conf.ServiceId)
|
||||
req.Header.Set("X-IM-Tenant-ID", fmt.Sprintf("%d", scope.TenantId))
|
||||
req.Header.Set("X-IM-Operator-ID", fmt.Sprintf("%d", scope.OperatorId))
|
||||
req.Header.Set("X-IM-Timestamp", timestamp)
|
||||
req.Header.Set("X-IM-Nonce", nonce)
|
||||
req.Header.Set("X-HotGo-Role-Key", scope.RoleKey)
|
||||
req.Header.Set("X-Request-ID", scope.RequestId)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) parseError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||||
var envelope struct {
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &envelope)
|
||||
return stableError(envelope.Message, envelope.Reason)
|
||||
}
|
||||
|
||||
func stableError(message string, reason string) error {
|
||||
if message != "" {
|
||||
return gerror.New(message)
|
||||
}
|
||||
if reason != "" {
|
||||
return gerror.New(reason)
|
||||
}
|
||||
return gerror.New("IM服务暂不可用")
|
||||
}
|
||||
|
||||
func hasInviteCode(in any) bool {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var m map[string]any
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return false
|
||||
}
|
||||
v, ok := m["inviteCode"]
|
||||
if ok && strings.TrimSpace(fmt.Sprint(v)) != "" {
|
||||
return true
|
||||
}
|
||||
v, ok = m["invite_code"]
|
||||
return ok && strings.TrimSpace(fmt.Sprint(v)) != ""
|
||||
}
|
||||
|
||||
func requestTarget(req *http.Request) string {
|
||||
target := req.URL.EscapedPath()
|
||||
if req.URL.RawQuery != "" {
|
||||
target += "?" + req.URL.RawQuery
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func randomNonce() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := cryptorand.Read(buf); err != nil {
|
||||
return "", gerror.Wrap(err, "生成IM请求随机数失败")
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func toQuery(in any) string {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var m map[string]any
|
||||
if err = json.Unmarshal(b, &m); err != nil {
|
||||
return ""
|
||||
}
|
||||
values := url.Values{}
|
||||
for key, value := range m {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
text := strings.TrimSpace(fmt.Sprint(value))
|
||||
if text == "" || text == "0" || text == "<nil>" {
|
||||
continue
|
||||
}
|
||||
values.Set(key, text)
|
||||
}
|
||||
return values.Encode()
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package imadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testInviteListRequest struct {
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
InviteCode string `json:"invite_code,omitempty"`
|
||||
BatchID int64 `json:"batch_id,omitempty"`
|
||||
}
|
||||
|
||||
func TestListCodesUsesSignedGetWithSnakeQuery(t *testing.T) {
|
||||
server := signedTestServer(t, http.MethodGet, "/admin/v1/im/registration-invite-codes", func(t *testing.T, r *http.Request, body []byte) {
|
||||
if len(body) != 0 {
|
||||
t.Fatalf("GET list should not send body, got %q", string(body))
|
||||
}
|
||||
query := r.URL.Query()
|
||||
if query.Get("page") != "2" || query.Get("per_page") != "20" || query.Get("batch_id") != "123" {
|
||||
t.Fatalf("unexpected query: %s", r.URL.RawQuery)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second})
|
||||
var out Page[struct{}]
|
||||
err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-1"}, testInviteListRequest{Page: 2, PerPage: 20, BatchID: 123}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCodes returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListCodesWithInviteCodeUsesSignedSearchPost(t *testing.T) {
|
||||
server := signedTestServer(t, http.MethodPost, "/admin/v1/im/registration-invite-codes/search", func(t *testing.T, r *http.Request, body []byte) {
|
||||
if !strings.Contains(string(body), `"invite_code":"Ab123"`) {
|
||||
t.Fatalf("POST search should send snake_case invite code, got %s", string(body))
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := NewClient(Config{BaseUrl: server.URL, ServiceId: "hotgo-admin", ServiceSecret: "secret", Timeout: time.Second})
|
||||
var out Page[struct{}]
|
||||
err := client.ListCodes(context.Background(), AdminScope{TenantId: 9, OperatorId: 7, RequestId: "req-2"}, testInviteListRequest{Page: 1, PerPage: 20, InviteCode: "Ab123"}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCodes returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func signedTestServer(t *testing.T, method string, path string, inspect func(t *testing.T, r *http.Request, body []byte)) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != method {
|
||||
t.Fatalf("method = %s, want %s", r.Method, method)
|
||||
}
|
||||
if r.URL.Path != path {
|
||||
t.Fatalf("path = %s, want %s", r.URL.Path, path)
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
inspect(t, r, body)
|
||||
assertSignedRequest(t, r, body, "secret")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":{"items":[],"page":1,"per_page":20,"total":0}}`))
|
||||
}))
|
||||
}
|
||||
|
||||
func assertSignedRequest(t *testing.T, r *http.Request, body []byte, secret string) {
|
||||
t.Helper()
|
||||
for _, name := range []string{"Authorization", "X-IM-Service-ID", "X-IM-Tenant-ID", "X-IM-Operator-ID", "X-IM-Timestamp", "X-IM-Nonce", "X-Request-ID"} {
|
||||
if r.Header.Get(name) == "" {
|
||||
t.Fatalf("missing header %s", name)
|
||||
}
|
||||
}
|
||||
if _, err := strconv.ParseInt(r.Header.Get("X-IM-Timestamp"), 10, 64); err != nil {
|
||||
t.Fatalf("timestamp is not unix seconds: %v", err)
|
||||
}
|
||||
bodyHash := sha256.Sum256(body)
|
||||
canonical := strings.Join([]string{
|
||||
r.Method,
|
||||
r.URL.RequestURI(),
|
||||
r.Header.Get("X-IM-Service-ID"),
|
||||
r.Header.Get("X-IM-Tenant-ID"),
|
||||
r.Header.Get("X-IM-Operator-ID"),
|
||||
r.Header.Get("X-IM-Timestamp"),
|
||||
r.Header.Get("X-IM-Nonce"),
|
||||
r.Header.Get("X-Request-ID"),
|
||||
hex.EncodeToString(bodyHash[:]),
|
||||
}, "\n")
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, _ = mac.Write([]byte(canonical))
|
||||
expected := fmt.Sprintf("IM-HMAC %s", hex.EncodeToString(mac.Sum(nil)))
|
||||
if r.Header.Get("Authorization") != expected {
|
||||
t.Fatalf("bad signature\nwant: %s\n got: %s", expected, r.Header.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package imadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
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 {
|
||||
timeout := g.Cfg().MustGet(ctx, "imAdminApi.timeout", "5s").Duration()
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
serviceSecret := g.Cfg().MustGet(ctx, "imAdminApi.serviceSecret").String()
|
||||
if strings.HasPrefix(serviceSecret, "${") && strings.HasSuffix(serviceSecret, "}") {
|
||||
serviceSecret = os.Getenv(strings.TrimSuffix(strings.TrimPrefix(serviceSecret, "${"), "}"))
|
||||
}
|
||||
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: 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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:"幂等键"`
|
||||
}
|
||||
@@ -21,6 +21,8 @@ var tenantDbGroups sync.Map
|
||||
|
||||
const registryDbGroup = "imRegistry"
|
||||
|
||||
var registryDbGroupOnce sync.Once
|
||||
|
||||
// DB resolves a tenant registry record and returns the corresponding business DB.
|
||||
func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
|
||||
if tenantId <= 0 {
|
||||
@@ -60,12 +62,34 @@ func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant
|
||||
// RegistryDB returns the dedicated tenant registry connection, or the default
|
||||
// HotGo database when a separate registry group is not configured.
|
||||
func RegistryDB() gdb.DB {
|
||||
registryDbGroupOnce.Do(func() {
|
||||
ensureRegistryDBGroup()
|
||||
})
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
return g.DB(registryDbGroup)
|
||||
}
|
||||
return g.DB()
|
||||
}
|
||||
|
||||
func ensureRegistryDBGroup() {
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
configs[0].Prefix = ""
|
||||
_ = gdb.SetConfigGroup(registryDbGroup, configs)
|
||||
return
|
||||
}
|
||||
|
||||
configs, err := gdb.GetConfigGroup("default")
|
||||
if err != nil || len(configs) == 0 {
|
||||
return
|
||||
}
|
||||
cloned := make(gdb.ConfigGroup, len(configs))
|
||||
copy(cloned, configs)
|
||||
for i := range cloned {
|
||||
cloned[i].Prefix = ""
|
||||
}
|
||||
_ = gdb.SetConfigGroup(registryDbGroup, cloned)
|
||||
}
|
||||
|
||||
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
|
||||
if tenant.DbDsn != "" {
|
||||
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"hotgo/internal/library/hgorm"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/adminin"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
@@ -469,7 +470,7 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
|
||||
if in.TenantId > 0 {
|
||||
count, queryErr := g.DB("imRegistry").Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
count, queryErr := imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||
Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive).
|
||||
Count()
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/imadmin"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type imAdminQueryPage struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
}
|
||||
|
||||
func resolveImAdminTenant(ctx context.Context, selectedTenantId int64) (tenant *entity.ImTenant, err error) {
|
||||
scope, err := resolveImAdminScope(ctx, selectedTenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(scope.TenantId).Scan(&tenant); err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 || tenant.Code == "" {
|
||||
return nil, gerror.New("IM租户不存在或编码为空")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func imAdminQueryList[T any](ctx context.Context, tenant *entity.ImTenant, path string, values url.Values, aliases map[string]string) (list []*T, totalCount int, err error) {
|
||||
if values == nil {
|
||||
values = url.Values{}
|
||||
}
|
||||
query := values.Encode()
|
||||
if query != "" {
|
||||
path += "?" + query
|
||||
}
|
||||
var page imAdminQueryPage
|
||||
if err = imadmin.DefaultClient(ctx).InternalGet(ctx, path, &page); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
normalizeImAdminRows(page.Items, aliases)
|
||||
if len(page.Items) == 0 {
|
||||
return []*T{}, page.Total, nil
|
||||
}
|
||||
raw, err := json.Marshal(page.Items)
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "转换IM内部查询列表失败")
|
||||
}
|
||||
if err = json.Unmarshal(raw, &list); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "解析IM内部查询列表失败")
|
||||
}
|
||||
fillImTenantFields(tenant, &list)
|
||||
return list, page.Total, nil
|
||||
}
|
||||
|
||||
func imAdminQueryOne[T any](ctx context.Context, tenant *entity.ImTenant, path string, aliases map[string]string) (res *T, err error) {
|
||||
row := map[string]any{}
|
||||
if err = imadmin.DefaultClient(ctx).InternalGet(ctx, path, &row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalizeImAdminRow(row, aliases)
|
||||
raw, err := json.Marshal(row)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "转换IM内部查询详情失败")
|
||||
}
|
||||
if err = json.Unmarshal(raw, &res); err != nil {
|
||||
return nil, gerror.Wrap(err, "解析IM内部查询详情失败")
|
||||
}
|
||||
fillImTenantFields(tenant, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func normalizeImAdminRows(rows []map[string]any, aliases map[string]string) {
|
||||
for _, row := range rows {
|
||||
normalizeImAdminRow(row, aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeImAdminRow(row map[string]any, aliases map[string]string) {
|
||||
for key, value := range row {
|
||||
target := snakeToLowerCamel(key)
|
||||
if aliases != nil {
|
||||
if alias := aliases[key]; alias != "" {
|
||||
target = alias
|
||||
}
|
||||
}
|
||||
value = normalizeImAdminValue(target, value)
|
||||
if target == key {
|
||||
row[key] = value
|
||||
continue
|
||||
}
|
||||
row[target] = value
|
||||
delete(row, key)
|
||||
}
|
||||
if _, ok := row["authNames"]; !ok {
|
||||
username, _ := row["username"].(string)
|
||||
phone, _ := row["phone"].(string)
|
||||
authNames := username
|
||||
if phone != "" && phone != username {
|
||||
if authNames != "" {
|
||||
authNames += ", "
|
||||
}
|
||||
authNames += phone
|
||||
}
|
||||
if authNames != "" {
|
||||
row["authNames"] = authNames
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeImAdminValue(target string, value any) any {
|
||||
if target != "credentials" {
|
||||
return value
|
||||
}
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
return value
|
||||
}
|
||||
parts := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
method, _ := m["method"].(string)
|
||||
val, _ := m["value"].(string)
|
||||
if method == "" && val == "" {
|
||||
continue
|
||||
}
|
||||
text := method
|
||||
if val != "" {
|
||||
if text != "" {
|
||||
text += ":"
|
||||
}
|
||||
text += val
|
||||
}
|
||||
if done, ok := m["done"].(bool); ok && !done {
|
||||
text += "(未验证)"
|
||||
}
|
||||
parts = append(parts, text)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func snakeToLowerCamel(s string) string {
|
||||
if !strings.Contains(s, "_") {
|
||||
return s
|
||||
}
|
||||
parts := strings.Split(s, "_")
|
||||
var b strings.Builder
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if i == 0 {
|
||||
b.WriteString(part)
|
||||
continue
|
||||
}
|
||||
b.WriteString(strings.ToUpper(part[:1]))
|
||||
if len(part) > 1 {
|
||||
b.WriteString(part[1:])
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func fillImTenantFields(tenant *entity.ImTenant, target any) {
|
||||
raw, err := json.Marshal(target)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var rows []map[string]any
|
||||
if err = json.Unmarshal(raw, &rows); err == nil {
|
||||
for _, row := range rows {
|
||||
row["tenantId"] = tenant.Id
|
||||
row["tenantName"] = tenant.Name
|
||||
}
|
||||
updated, _ := json.Marshal(rows)
|
||||
_ = json.Unmarshal(updated, target)
|
||||
return
|
||||
}
|
||||
var row map[string]any
|
||||
if err = json.Unmarshal(raw, &row); err == nil {
|
||||
row["tenantId"] = tenant.Id
|
||||
row["tenantName"] = tenant.Name
|
||||
updated, _ := json.Marshal(row)
|
||||
_ = json.Unmarshal(updated, target)
|
||||
}
|
||||
}
|
||||
|
||||
func addPageQuery(values url.Values, page int, perPage int) url.Values {
|
||||
if values == nil {
|
||||
values = url.Values{}
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if perPage <= 0 {
|
||||
perPage = 20
|
||||
}
|
||||
values.Set("page", intToString(page))
|
||||
values.Set("per_page", intToString(perPage))
|
||||
return values
|
||||
}
|
||||
|
||||
func addTimeRangeQuery(values url.Values, prefix string, times []*gtime.Time) {
|
||||
if len(times) != 2 {
|
||||
return
|
||||
}
|
||||
if times[0] != nil {
|
||||
values.Set(prefix+"_start", times[0].Time.UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
if times[1] != nil {
|
||||
values.Set(prefix+"_end", times[1].Time.UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
}
|
||||
|
||||
func intToString(v int) string {
|
||||
return strconvFormatInt(int64(v))
|
||||
}
|
||||
|
||||
func int64ToString(v int64) string {
|
||||
return strconvFormatInt(v)
|
||||
}
|
||||
|
||||
func strconvFormatInt(v int64) string {
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
@@ -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().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,
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@@ -385,6 +387,22 @@ func (s *sSysImGroup) GovernanceUpdate(ctx context.Context, in *sysin.ImGroupGov
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) ProfileUpdate(ctx context.Context, in *sysin.ImGroupProfileUpdateInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"profile": map[string]any{
|
||||
"name": in.Name,
|
||||
"avatar_file_id": in.AvatarFileId,
|
||||
"description": in.Description,
|
||||
},
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupProfileUpdate, payload, in.Version)
|
||||
}
|
||||
|
||||
// Dissolve 解散IM群
|
||||
func (s *sSysImGroup) Dissolve(ctx context.Context, in *sysin.ImGroupDissolveInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
@@ -483,6 +501,22 @@ func (s *sSysImGroup) Mute(ctx context.Context, in *sysin.ImGroupMuteInp) (err e
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) MemberMute(ctx context.Context, in *sysin.ImGroupMuteInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"user_id": strconv.FormatInt(in.UserId, 10),
|
||||
"reason": in.Reason,
|
||||
}
|
||||
if in.ExpiresAt != nil {
|
||||
payload["expires_at"] = in.ExpiresAt.Time.Format("2006-01-02T15:04:05.000Z07:00")
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupMemberMute, payload, 0)
|
||||
}
|
||||
|
||||
// Unmute 解除IM群成员禁言
|
||||
func (s *sSysImGroup) Unmute(ctx context.Context, in *sysin.ImGroupUnmuteInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
@@ -529,6 +563,31 @@ func (s *sSysImGroup) Unmute(ctx context.Context, in *sysin.ImGroupUnmuteInp) (e
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) MemberUnmute(ctx context.Context, in *sysin.ImGroupUnmuteInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"user_id": strconv.FormatInt(in.UserId, 10),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupMemberUnmute, payload, 0)
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) MemberRemove(ctx context.Context, in *sysin.ImGroupMemberRemoveInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"user_id": strconv.FormatInt(in.UserId, 10),
|
||||
"reason": in.Reason,
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupMemberRemove, payload, 0)
|
||||
}
|
||||
|
||||
// JoinRequestApprove 通过IM入群申请
|
||||
func (s *sSysImGroup) JoinRequestApprove(ctx context.Context, in *sysin.ImGroupJoinRequestResolveInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
@@ -683,6 +742,30 @@ func (s *sSysImGroup) RoleUpdate(ctx context.Context, in *sysin.ImGroupRoleUpdat
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) AdminSet(ctx context.Context, in *sysin.ImGroupRoleUpdateInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"user_ids": []string{strconv.FormatInt(in.UserId, 10)},
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupAdminSet, payload, 0)
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) AdminRemove(ctx context.Context, in *sysin.ImGroupRoleUpdateInp) (res *sysin.ImGroupCommandModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := map[string]any{
|
||||
"topic": in.Topic,
|
||||
"actor_user_id": strconv.FormatInt(contexts.GetUserId(ctx), 10),
|
||||
"user_id": strconv.FormatInt(in.UserId, 10),
|
||||
}
|
||||
return s.publishGroupCommand(ctx, in.TenantId, in.Topic, consts.ImAdminCommandGroupAdminRemove, payload, 0)
|
||||
}
|
||||
|
||||
// OwnerTransfer 转让IM群主
|
||||
func (s *sSysImGroup) OwnerTransfer(ctx context.Context, in *sysin.ImGroupOwnerTransferInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
@@ -785,6 +868,45 @@ func (s *sSysImGroup) OwnerTransfer(ctx context.Context, in *sysin.ImGroupOwnerT
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) publishGroupCommand(ctx context.Context, tenantId int64, topic string, eventType string, payload map[string]any, version int64) (*sysin.ImGroupCommandModel, error) {
|
||||
_, tenant, err := resolveImTenantDB(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 (s *sSysImGroup) groupGovernanceSnapshot(ctx context.Context, tx gdb.TX, topic string) (res gdb.Record, err error) {
|
||||
return tx.Model("im_group_settings").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -25,8 +25,6 @@ import (
|
||||
|
||||
type sSysImTenant struct{}
|
||||
|
||||
const imRegistryDbGroup = "imRegistry"
|
||||
|
||||
func NewSysImTenant() *sSysImTenant {
|
||||
return &sSysImTenant{}
|
||||
}
|
||||
@@ -37,11 +35,11 @@ func init() {
|
||||
|
||||
// Model IM租户ORM模型
|
||||
func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
return handler.Model(g.DB(imRegistryDbGroup).Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
|
||||
return handler.Model(imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
|
||||
return g.DB(imRegistryDbGroup).Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
|
||||
return imtenant.RegistryDB().Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// List 获取IM租户列表
|
||||
@@ -146,7 +144,7 @@ func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err
|
||||
return
|
||||
}
|
||||
|
||||
return g.DB(imRegistryDbGroup).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
return imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,9 +12,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -40,36 +44,34 @@ func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
values := addPageQuery(url.Values{}, in.Page, in.PerPage)
|
||||
if in.UserId != "" {
|
||||
values.Set("user_id", in.UserId)
|
||||
}
|
||||
|
||||
mod := s.listModel(ctx, db)
|
||||
mod = s.applyListWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("u.id")
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM用户列表失败,请稍后重试!")
|
||||
return
|
||||
if in.Nickname != "" {
|
||||
values.Set("nickname", in.Nickname)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return
|
||||
if in.AuthName != "" {
|
||||
values.Set("auth_name", in.AuthName)
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
item.TenantId = tenant.Id
|
||||
item.TenantName = tenant.Name
|
||||
if in.State != nil {
|
||||
values.Set("state", intToString(*in.State))
|
||||
}
|
||||
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
addTimeRangeQuery(values, "created_at", in.CreatedAt)
|
||||
addTimeRangeQuery(values, "last_seen", in.LastSeen)
|
||||
return imAdminQueryList[sysin.ImUserListModel](ctx, tenant,
|
||||
"/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users",
|
||||
values,
|
||||
map[string]string{
|
||||
"public": "profileJson",
|
||||
"trusted": "trustedJson",
|
||||
"access": "accessJson",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
@@ -120,13 +122,16 @@ func (s *sSysImUser) fillAuthAndCredentials(ctx context.Context, db gdb.DB, list
|
||||
ids := make([]int64, 0, len(list))
|
||||
userMap := make(map[int64]*sysin.ImUserListModel, len(list))
|
||||
for _, item := range list {
|
||||
userId, parseErr := sysin.ParseOptionalInt64(item.UserId)
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
userId := item.UserId
|
||||
if userId <= 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, userId)
|
||||
userMap[userId] = item
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
credentialRows, err := db.Model("im_credentials").Safe().Ctx(ctx).
|
||||
Fields("userid", "method", "value", "done").
|
||||
@@ -164,41 +169,18 @@ func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sy
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := sysin.ParseOptionalInt64(in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields := append(s.viewFields(), "u.access access_json", "u.public profile_json", "u.trusted trusted_json")
|
||||
err = db.Model("im_users u").Safe().Ctx(ctx).
|
||||
LeftJoin("im_user_profiles up", "up.user_id = u.id").
|
||||
LeftJoin("im_user_security us", "us.user_id = u.id").
|
||||
Fields(fields...).
|
||||
Where("u.id", userId).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM用户信息失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
list := []*sysin.ImUserListModel{&res.ImUserListModel}
|
||||
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
return imAdminQueryOne[sysin.ImUserViewModel](ctx, tenant,
|
||||
"/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users/"+url.PathEscape(in.UserId),
|
||||
map[string]string{
|
||||
"public": "profileJson",
|
||||
"trusted": "trustedJson",
|
||||
"access": "accessJson",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) viewFields() []interface{} {
|
||||
@@ -306,45 +288,104 @@ func (s *sSysImUser) Devices(ctx context.Context, in *sysin.ImUserDeviceListInp)
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userId, err := sysin.ParseOptionalInt64(in.UserId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
|
||||
tenant, err := resolveImAdminTenant(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return imAdminQueryList[sysin.ImUserDeviceListModel](ctx, tenant,
|
||||
"/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/users/"+url.PathEscape(in.UserId)+"/devices",
|
||||
addPageQuery(url.Values{}, in.Page, in.PerPage),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
mod := db.Model("im_devices d").Safe().Ctx(ctx).
|
||||
LeftJoin("im_device_push_tokens p", "p.user_id=d.userid AND p.device_id=d.deviceid").
|
||||
Fields(
|
||||
"CAST(d.userid AS CHAR) user_id",
|
||||
"d.deviceid device_id",
|
||||
"COALESCE(d.platform, '') platform",
|
||||
"COALESCE(d.lang, '') lang",
|
||||
"d.lastseen last_seen",
|
||||
"COALESCE(p.provider, '') push_provider",
|
||||
"COALESCE(p.platform, '') push_platform",
|
||||
"COALESCE(p.app_version, '') push_app_version",
|
||||
"(p.id IS NOT NULL) push_token_configured",
|
||||
"p.updated_at push_updated_at",
|
||||
).
|
||||
Where("d.userid", userId).
|
||||
Page(in.Page, in.PerPage).
|
||||
OrderDesc("d.lastseen")
|
||||
func (s *sSysImUser) Ban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserBan)
|
||||
}
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "获取IM用户设备列表失败,请稍后重试!")
|
||||
func (s *sSysImUser) Unban(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnban)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) Freeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserFreeze)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) Unfreeze(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserUnfreeze)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) DeviceKick(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
if err = in.FilterDevice(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
item.TenantId = tenant.Id
|
||||
item.TenantName = tenant.Name
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserDeviceKick)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) LogoutAll(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserLogoutAll)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) AccountCancel(ctx context.Context, in *sysin.ImUserGovernanceInp) (res *sysin.ImUserCommandModel, err error) {
|
||||
return s.publishGovernanceCommand(ctx, in, consts.ImAdminCommandUserCancel)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return
|
||||
_, 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 updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, state int) error {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -198,6 +198,30 @@ func (in *ImGroupGovernanceUpdateInp) Filter(ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupProfileUpdateInp struct {
|
||||
ImGroupTopicInp
|
||||
Name string `json:"name" dc:"群名称"`
|
||||
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
|
||||
Description string `json:"description" dc:"简介"`
|
||||
Version int64 `json:"version" dc:"版本"`
|
||||
}
|
||||
|
||||
func (in *ImGroupProfileUpdateInp) Filter(ctx context.Context) (err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if in.Name == "" && in.AvatarFileId == "" && in.Description == "" {
|
||||
return gerror.New("请填写需要修改的群资料")
|
||||
}
|
||||
if len([]rune(in.Name)) > 80 {
|
||||
return gerror.New("群名称不能超过80个字符")
|
||||
}
|
||||
if len([]rune(in.Description)) > 500 {
|
||||
return gerror.New("群简介不能超过500个字符")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupDissolveInp struct {
|
||||
ImGroupTopicInp
|
||||
Reason string `json:"reason" dc:"解散原因"`
|
||||
@@ -458,3 +482,32 @@ type ImGroupAdminLogListModel struct {
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
type ImGroupMemberRemoveInp struct {
|
||||
ImGroupTopicInp
|
||||
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
|
||||
Reason string `json:"reason" dc:"原因"`
|
||||
}
|
||||
|
||||
func (in *ImGroupMemberRemoveInp) Filter(ctx context.Context) (err error) {
|
||||
if err = in.ImGroupTopicInp.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if in.UserId <= 0 {
|
||||
return gerror.New("用户ID不能为空")
|
||||
}
|
||||
if len([]rune(in.Reason)) > 200 {
|
||||
return gerror.New("原因不能超过200个字符")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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:"幂等键"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func (in *ImUserListInp) Filter(ctx context.Context) (err error) {
|
||||
type ImUserListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
UserId string `json:"userId" dc:"用户ID"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
Username string `json:"username" dc:"登录账号"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
@@ -139,7 +139,7 @@ func (in *ImUserDeviceListInp) Filter(ctx context.Context) (err error) {
|
||||
type ImUserDeviceListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
UserId string `json:"userId" dc:"用户ID"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
DeviceId string `json:"deviceId" dc:"设备ID"`
|
||||
Platform string `json:"platform" dc:"平台"`
|
||||
Lang string `json:"lang" dc:"语言"`
|
||||
@@ -150,3 +150,45 @@ type ImUserDeviceListModel struct {
|
||||
PushTokenConfigured bool `json:"pushTokenConfigured" dc:"是否配置Push Token"`
|
||||
PushUpdatedAt *gtime.Time `json:"pushUpdatedAt" dc:"Push更新时间"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type ImUserCommandModel 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:"幂等键"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 result imkafka.CommandResult
|
||||
if err = json.Unmarshal(mqMsg.Body, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
if result.CommandID == "" && result.CommandEventID == "" {
|
||||
var envelope imkafka.Envelope
|
||||
if err = json.Unmarshal(mqMsg.Body, &envelope); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(envelope.Payload) > 0 {
|
||||
if err = json.Unmarshal(envelope.Payload, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
result.EventID = envelope.EventID
|
||||
result.TraceID = envelope.TraceID
|
||||
result.TenantCode = envelope.TenantCode
|
||||
result.OccurredAt = envelope.OccurredAt
|
||||
}
|
||||
}
|
||||
return imkafka.ApplyCommandResult(ctx, result)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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": stringValue(payload["dead_id"]),
|
||||
"reason": firstString(payload["reason"], payload["last_reason"]),
|
||||
"message": firstString(payload["message"], payload["last_error"]),
|
||||
"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后台命令
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -226,6 +226,22 @@ type (
|
||||
Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error)
|
||||
// Devices 获取IM用户设备列表
|
||||
Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, 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群组列表
|
||||
@@ -238,18 +254,30 @@ type (
|
||||
Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error)
|
||||
// GovernanceUpdate 更新IM群治理配置
|
||||
GovernanceUpdate(ctx context.Context, in *sysin.ImGroupGovernanceUpdateInp) (err error)
|
||||
// ProfileUpdate 修改IM群资料
|
||||
ProfileUpdate(ctx context.Context, in *sysin.ImGroupProfileUpdateInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// Dissolve 解散IM群
|
||||
Dissolve(ctx context.Context, in *sysin.ImGroupDissolveInp) (err error)
|
||||
// Mute 禁言IM群成员
|
||||
Mute(ctx context.Context, in *sysin.ImGroupMuteInp) (err error)
|
||||
// MemberMute Kafka禁言IM群成员
|
||||
MemberMute(ctx context.Context, in *sysin.ImGroupMuteInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// Unmute 解除IM群成员禁言
|
||||
Unmute(ctx context.Context, in *sysin.ImGroupUnmuteInp) (err error)
|
||||
// MemberUnmute Kafka解除IM群成员禁言
|
||||
MemberUnmute(ctx context.Context, in *sysin.ImGroupUnmuteInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// MemberRemove 移出IM群成员
|
||||
MemberRemove(ctx context.Context, in *sysin.ImGroupMemberRemoveInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// JoinRequestApprove 通过IM入群申请
|
||||
JoinRequestApprove(ctx context.Context, in *sysin.ImGroupJoinRequestResolveInp) (err error)
|
||||
// JoinRequestReject 拒绝IM入群申请
|
||||
JoinRequestReject(ctx context.Context, in *sysin.ImGroupJoinRequestResolveInp) (err error)
|
||||
// RoleUpdate 更新IM群成员角色
|
||||
RoleUpdate(ctx context.Context, in *sysin.ImGroupRoleUpdateInp) (err error)
|
||||
// AdminSet 设置IM群管理员
|
||||
AdminSet(ctx context.Context, in *sysin.ImGroupRoleUpdateInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// AdminRemove 删除IM群管理员
|
||||
AdminRemove(ctx context.Context, in *sysin.ImGroupRoleUpdateInp) (res *sysin.ImGroupCommandModel, err error)
|
||||
// OwnerTransfer 转让IM群主
|
||||
OwnerTransfer(ctx context.Context, in *sysin.ImGroupOwnerTransferInp) (err error)
|
||||
// JoinRequestList 获取IM入群申请列表
|
||||
@@ -395,6 +423,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
|
||||
@@ -658,6 +714,9 @@ var (
|
||||
localSysImWorkbench ISysImWorkbench
|
||||
localSysImTaskDashboard ISysImTaskDashboard
|
||||
localSysImAuditLog ISysImAuditLog
|
||||
localSysImKafkaAudit ISysImKafkaAudit
|
||||
localSysImKafkaCommand ISysImKafkaCommand
|
||||
localSysImDeadLetter ISysImDeadLetter
|
||||
localSysDictData ISysDictData
|
||||
localSysDictType ISysDictType
|
||||
localSysEmsLog ISysEmsLog
|
||||
@@ -981,6 +1040,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?")
|
||||
|
||||
@@ -305,6 +305,18 @@ hggen:
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
|
||||
imDirectDb:
|
||||
# Each HotGo writer must use a unique ID which does not overlap an IM node.
|
||||
snowflakeWorkerId: 1023
|
||||
imAdminApi:
|
||||
baseUrl: "http://127.0.0.1:6060"
|
||||
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');
|
||||
@@ -1,3 +1,5 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @now = NOW();
|
||||
|
||||
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
|
||||
@@ -36,50 +38,10 @@ INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type
|
||||
SELECT NULL, @groupListId, '群治理配置', 'imGroupGovernance', '', '', '3', '', '/imGroup/governance', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '30', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupGovernance');
|
||||
|
||||
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, ' '), '40', '', '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, '解散群', 'imGroupDissolve', '', '', '3', '', '/imGroup/dissolve', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '50', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupDissolve');
|
||||
|
||||
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, '禁言群成员', 'imGroupMute', '', '', '3', '', '/imGroup/mute', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '60', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMute');
|
||||
|
||||
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, '解除成员禁言', 'imGroupUnmute', '', '', '3', '', '/imGroup/unmute', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '70', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupUnmute');
|
||||
|
||||
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, '通过入群申请', 'imGroupJoinRequestApprove', '', '', '3', '', '/imGroup/joinRequestApprove', '', '', '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` = 'imGroupJoinRequestApprove');
|
||||
|
||||
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, '拒绝入群申请', 'imGroupJoinRequestReject', '', '', '3', '', '/imGroup/joinRequestReject', '', '', '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` = 'imGroupJoinRequestReject');
|
||||
|
||||
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, '更新群成员角色', 'imGroupRoleUpdate', '', '', '3', '', '/imGroup/roleUpdate', '', '', '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` = 'imGroupRoleUpdate');
|
||||
|
||||
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, ' '), '110', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupOwnerTransfer');
|
||||
|
||||
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, '入群申请列表', 'imGroupJoinRequestList', '', '', '3', '', '/imGroup/joinRequestList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '40', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupJoinRequestList');
|
||||
|
||||
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, '群文件列表', 'imGroupFileList', '', '', '3', '', '/imGroup/fileList', '', '', '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` = 'imGroupFileList');
|
||||
|
||||
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`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
|
||||
SELECT NULL, @groupListId, '删除群文件', 'imGroupFileDelete', '', '', '3', '', '/imGroup/fileDelete', '', '', '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` = 'imGroupFileDelete');
|
||||
|
||||
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, '群邀请列表', 'imGroupInvitationList', '', '', '3', '', '/imGroup/invitationList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '50', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupInvitationList');
|
||||
@@ -95,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审计事件';
|
||||
@@ -1,3 +1,5 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @now = NOW();
|
||||
|
||||
SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
|
||||
@@ -29,17 +31,33 @@ SELECT NULL, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUse
|
||||
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, '重置用户密码', 'imUserResetPassword', '', '', '3', '', '/imUser/resetPassword', '', '', '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` = 'imUserResetPassword');
|
||||
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, '用户设备列表', 'imUserDevices', '', '', '3', '', '/imUser/devices', '', '', '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` = 'imUserDevices');
|
||||
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, '撤销用户会话', 'imUserRevokeSessions', '', '', '3', '', '/imUser/revokeSessions', '', '', '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` = 'imUserRevokeSessions');
|
||||
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, '更新用户状态', 'imUserStatus', '', '', '3', '', '/imUser/status', '', '', '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` = 'imUserStatus');
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user