feat: add IM admin management

This commit is contained in:
richjohnrambo
2026-08-21 01:06:19 +08:00
parent 331a55751d
commit a2abaef3a6
290 changed files with 19917 additions and 337 deletions
@@ -0,0 +1,33 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package imauditlog
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM审计日志列表
type ListReq struct {
g.Meta `path:"/imAuditLog/list" method:"get" tags:"IM审计日志" summary:"获取IM审计日志列表"`
sysin.ImAuditLogListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImAuditLogListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM审计日志指定信息
type ViewReq struct {
g.Meta `path:"/imAuditLog/view" method:"get" tags:"IM审计日志" summary:"获取IM审计日志指定信息"`
sysin.ImAuditLogViewInp
}
type ViewRes struct {
*sysin.ImAuditLogViewModel
}
+110
View File
@@ -0,0 +1,110 @@
// Package imgroup
// @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 imgroup
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM群组列表
type ListReq struct {
g.Meta `path:"/imGroup/list" method:"get" tags:"IM群组" summary:"获取IM群组列表"`
sysin.ImGroupListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImGroupListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM群组指定信息
type ViewReq struct {
g.Meta `path:"/imGroup/view" method:"get" tags:"IM群组" summary:"获取IM群组指定信息"`
sysin.ImGroupViewInp
}
type ViewRes struct {
*sysin.ImGroupViewModel
}
// MemberListReq 查询IM群成员列表
type MemberListReq struct {
g.Meta `path:"/imGroup/memberList" method:"get" tags:"IM群组" summary:"获取IM群成员列表"`
sysin.ImGroupMemberListInp
}
type MemberListRes struct {
form.PageRes
List []*sysin.ImGroupMemberListModel `json:"list" dc:"数据列表"`
}
// GovernanceReq 获取IM群治理配置
type GovernanceReq struct {
g.Meta `path:"/imGroup/governance" method:"get" tags:"IM群组" summary:"获取IM群治理配置"`
sysin.ImGroupTopicInp
}
type GovernanceRes struct {
*sysin.ImGroupGovernanceModel
}
// JoinRequestListReq 查询IM入群申请列表
type JoinRequestListReq struct {
g.Meta `path:"/imGroup/joinRequestList" method:"get" tags:"IM群组" summary:"获取IM入群申请列表"`
sysin.ImGroupJoinRequestListInp
}
type JoinRequestListRes struct {
form.PageRes
List []*sysin.ImGroupJoinRequestListModel `json:"list" dc:"数据列表"`
}
// InvitationListReq 查询IM群邀请列表
type InvitationListReq struct {
g.Meta `path:"/imGroup/invitationList" method:"get" tags:"IM群组" summary:"获取IM群邀请列表"`
sysin.ImGroupInvitationListInp
}
type InvitationListRes struct {
form.PageRes
List []*sysin.ImGroupInvitationListModel `json:"list" dc:"数据列表"`
}
// AnnouncementListReq 查询IM群公告列表
type AnnouncementListReq struct {
g.Meta `path:"/imGroup/announcementList" method:"get" tags:"IM群组" summary:"获取IM群公告列表"`
sysin.ImGroupAnnouncementListInp
}
type AnnouncementListRes struct {
form.PageRes
List []*sysin.ImGroupAnnouncementListModel `json:"list" dc:"数据列表"`
}
// MuteListReq 查询IM群禁言列表
type MuteListReq struct {
g.Meta `path:"/imGroup/muteList" method:"get" tags:"IM群组" summary:"获取IM群禁言列表"`
sysin.ImGroupMuteListInp
}
type MuteListRes struct {
form.PageRes
List []*sysin.ImGroupMuteListModel `json:"list" dc:"数据列表"`
}
// AdminLogListReq 查询IM群管理日志列表
type AdminLogListReq struct {
g.Meta `path:"/imGroup/adminLogList" method:"get" tags:"IM群组" summary:"获取IM群管理日志列表"`
sysin.ImGroupAdminLogListInp
}
type AdminLogListRes struct {
form.PageRes
List []*sysin.ImGroupAdminLogListModel `json:"list" dc:"数据列表"`
}
+68
View File
@@ -0,0 +1,68 @@
package iminvite
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type ListReq struct {
g.Meta `path:"/imInvite/list" method:"post" tags:"IM邀请码" summary:"获取IM注册邀请码列表"`
sysin.ImInviteListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImInviteCodeModel `json:"list" dc:"数据列表"`
}
type CreateReq struct {
g.Meta `path:"/imInvite/create" method:"post" tags:"IM邀请码" summary:"生成IM注册邀请码"`
sysin.ImInviteCreateInp
}
type CreateRes struct {
*sysin.ImInviteCodeModel
}
type BatchCreateReq struct {
g.Meta `path:"/imInvite/batchCreate" method:"post" tags:"IM邀请码" summary:"批量生成IM注册邀请码"`
sysin.ImInviteBatchCreateInp
}
type BatchCreateRes struct {
*sysin.ImInviteBatchModel
}
type BatchViewReq struct {
g.Meta `path:"/imInvite/batchView" method:"get" tags:"IM邀请码" summary:"查看IM注册邀请码批次"`
sysin.ImInviteBatchViewInp
}
type BatchViewRes struct {
*sysin.ImInviteBatchDetailModel
}
type RevokeReq struct {
g.Meta `path:"/imInvite/revoke" method:"post" tags:"IM邀请码" summary:"撤销IM注册邀请码"`
sysin.ImInviteRevokeInp
}
type RevokeRes struct{}
type BatchRevokeReq struct {
g.Meta `path:"/imInvite/batchRevoke" method:"post" tags:"IM邀请码" summary:"批量撤销IM注册邀请码"`
sysin.ImInviteBatchRevokeInp
}
type BatchRevokeRes struct {
*sysin.ImInviteBatchModel
}
type ExportReq struct {
g.Meta `path:"/imInvite/export" method:"post" tags:"IM邀请码" summary:"导出IM注册邀请码"`
sysin.ImInviteListInp
}
type ExportRes struct{}
@@ -0,0 +1,42 @@
// Package imrealname
// @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 imrealname
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM实名审核列表
type ListReq struct {
g.Meta `path:"/imRealname/list" method:"get" tags:"IM实名审核" summary:"获取IM实名审核列表"`
sysin.ImRealnameListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImRealnameListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM实名审核指定信息
type ViewReq struct {
g.Meta `path:"/imRealname/view" method:"get" tags:"IM实名审核" summary:"获取IM实名审核指定信息"`
sysin.ImRealnameViewInp
}
type ViewRes struct {
*sysin.ImRealnameViewModel
}
// ReviewReq 审核IM实名
type ReviewReq struct {
g.Meta `path:"/imRealname/review" method:"post" tags:"IM实名审核" summary:"审核IM实名"`
sysin.ImRealnameReviewInp
}
type ReviewRes struct{}
@@ -0,0 +1,22 @@
package imregistration
import (
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
type GetReq struct {
g.Meta `path:"/imRegistration/config/get" method:"get" tags:"IM注册" summary:"获取IM注册配置"`
}
type GetRes struct {
*sysin.ImRegistrationConfigModel
}
type UpdateReq struct {
g.Meta `path:"/imRegistration/config/update" method:"post" tags:"IM注册" summary:"更新IM注册配置"`
sysin.ImRegistrationConfigUpdateInp
}
type UpdateRes struct{}
+50
View File
@@ -0,0 +1,50 @@
// Package imtenant
// @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 imtenant
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM租户列表
type ListReq struct {
g.Meta `path:"/imTenant/list" method:"get" tags:"IM租户" summary:"获取IM租户列表"`
sysin.ImTenantListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImTenantListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM租户指定信息
type ViewReq struct {
g.Meta `path:"/imTenant/view" method:"get" tags:"IM租户" summary:"获取IM租户指定信息"`
sysin.ImTenantViewInp
}
type ViewRes struct {
*sysin.ImTenantViewModel
}
// EditReq 修改/新增IM租户
type EditReq struct {
g.Meta `path:"/imTenant/edit" method:"post" tags:"IM租户" summary:"修改/新增IM租户"`
sysin.ImTenantEditInp
}
type EditRes struct{}
// StatusReq 更新IM租户状态
type StatusReq struct {
g.Meta `path:"/imTenant/status" method:"post" tags:"IM租户" summary:"更新IM租户状态"`
sysin.ImTenantStatusInp
}
type StatusRes struct{}
+34
View File
@@ -0,0 +1,34 @@
// Package imuser
// @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 imuser
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM用户列表
type ListReq struct {
g.Meta `path:"/imUser/list" method:"get" tags:"IM用户" summary:"获取IM用户列表"`
sysin.ImUserListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImUserListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM用户指定信息
type ViewReq struct {
g.Meta `path:"/imUser/view" method:"get" tags:"IM用户" summary:"获取IM用户指定信息"`
sysin.ImUserViewInp
}
type ViewRes struct {
*sysin.ImUserViewModel
}
@@ -0,0 +1,42 @@
// Package imworkorder
// @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 imworkorder
import (
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询IM举报反馈工单列表
type ListReq struct {
g.Meta `path:"/imWorkOrder/list" method:"get" tags:"IM举报反馈" summary:"获取IM举报反馈工单列表"`
sysin.ImWorkOrderListInp
}
type ListRes struct {
form.PageRes
List []*sysin.ImWorkOrderListModel `json:"list" dc:"数据列表"`
}
// ViewReq 获取IM举报反馈工单指定信息
type ViewReq struct {
g.Meta `path:"/imWorkOrder/view" method:"get" tags:"IM举报反馈" summary:"获取IM举报反馈工单指定信息"`
sysin.ImWorkOrderViewInp
}
type ViewRes struct {
*sysin.ImWorkOrderViewModel
}
// StatusReq 更新IM举报反馈工单状态
type StatusReq struct {
g.Meta `path:"/imWorkOrder/status" method:"post" tags:"IM举报反馈" summary:"更新IM举报反馈工单状态"`
sysin.ImWorkOrderStatusInp
}
type StatusRes struct{}
+1 -1
View File
@@ -22,7 +22,7 @@ gfcli:
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
- link: "${HGGEN_DAO_LINK}"
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
group: "default" # 分组 使用hotgo代码生成功能时必须填
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
+105
View File
@@ -0,0 +1,105 @@
// 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("ImGroupStateOptions", "IM群组状态选项", ImGroupStateOptions)
dict.RegisterEnums("ImGroupStatusOptions", "IM群业务状态选项", ImGroupStatusOptions)
dict.RegisterEnums("ImGroupRoleOptions", "IM群成员角色选项", ImGroupRoleOptions)
dict.RegisterEnums("ImGroupMemberStatusOptions", "IM群成员状态选项", ImGroupMemberStatusOptions)
dict.RegisterEnums("ImGroupRequestStatusOptions", "IM入群申请状态选项", ImGroupRequestStatusOptions)
dict.RegisterEnums("ImGroupMuteStatusOptions", "IM禁言状态选项", ImGroupMuteStatusOptions)
dict.RegisterEnums("ImGroupAnnouncementStatusOptions", "IM群公告状态选项", ImGroupAnnouncementStatusOptions)
dict.RegisterEnums("ImGroupOutboxStatusOptions", "IM群事件状态选项", ImGroupOutboxStatusOptions)
}
var ImGroupStateOptions = []*model.Option{
dict.GenSuccessOption(ImUserStateOk, "正常"),
dict.GenErrorOption(ImUserStateSuspended, "已停用"),
dict.GenDefaultOption(ImUserStateDeleted, "已删除"),
dict.GenWarningOption(ImUserStateUndefined, "未定义"),
}
const (
ImGroupStatusActive = "active"
ImGroupStatusDissolved = "dissolved"
ImGroupStatusDisabled = "disabled"
ImGroupRoleOwner = "owner"
ImGroupRoleAdmin = "admin"
ImGroupRoleMember = "member"
ImGroupMemberStatusActive = "active"
ImGroupMemberStatusRemoved = "removed"
ImGroupMemberStatusLeft = "left"
ImGroupRequestStatusPending = "pending"
ImGroupRequestStatusAccepted = "accepted"
ImGroupRequestStatusRejected = "rejected"
ImGroupRequestStatusExpired = "expired"
ImGroupRequestStatusCanceled = "canceled"
ImGroupMuteStatusActive = "active"
ImGroupMuteStatusRevoked = "revoked"
ImGroupMuteStatusExpired = "expired"
ImGroupAnnouncementStatusCurrent = "current"
ImGroupAnnouncementStatusHistory = "history"
ImGroupAnnouncementStatusDeleted = "deleted"
ImGroupOutboxStatusPending = "pending"
ImGroupOutboxStatusDelivered = "delivered"
ImGroupOutboxStatusFailed = "failed"
)
var ImGroupStatusOptions = []*model.Option{
dict.GenSuccessOption(ImGroupStatusActive, "正常"),
dict.GenDefaultOption(ImGroupStatusDissolved, "已解散"),
dict.GenErrorOption(ImGroupStatusDisabled, "已停用"),
}
var ImGroupRoleOptions = []*model.Option{
dict.GenPrimaryOption(ImGroupRoleOwner, "群主"),
dict.GenSuccessOption(ImGroupRoleAdmin, "管理员"),
dict.GenDefaultOption(ImGroupRoleMember, "成员"),
}
var ImGroupMemberStatusOptions = []*model.Option{
dict.GenSuccessOption(ImGroupMemberStatusActive, "正常"),
dict.GenDefaultOption(ImGroupMemberStatusRemoved, "已移除"),
dict.GenWarningOption(ImGroupMemberStatusLeft, "已退出"),
}
var ImGroupRequestStatusOptions = []*model.Option{
dict.GenWarningOption(ImGroupRequestStatusPending, "待处理"),
dict.GenSuccessOption(ImGroupRequestStatusAccepted, "已同意"),
dict.GenErrorOption(ImGroupRequestStatusRejected, "已拒绝"),
dict.GenDefaultOption(ImGroupRequestStatusExpired, "已过期"),
dict.GenDefaultOption(ImGroupRequestStatusCanceled, "已取消"),
}
var ImGroupMuteStatusOptions = []*model.Option{
dict.GenErrorOption(ImGroupMuteStatusActive, "禁言中"),
dict.GenDefaultOption(ImGroupMuteStatusRevoked, "已解除"),
dict.GenWarningOption(ImGroupMuteStatusExpired, "已过期"),
}
var ImGroupAnnouncementStatusOptions = []*model.Option{
dict.GenSuccessOption(ImGroupAnnouncementStatusCurrent, "当前"),
dict.GenDefaultOption(ImGroupAnnouncementStatusHistory, "历史"),
dict.GenErrorOption(ImGroupAnnouncementStatusDeleted, "已删除"),
}
var ImGroupOutboxStatusOptions = []*model.Option{
dict.GenWarningOption(ImGroupOutboxStatusPending, "待投递"),
dict.GenSuccessOption(ImGroupOutboxStatusDelivered, "已投递"),
dict.GenErrorOption(ImGroupOutboxStatusFailed, "投递失败"),
}
+44
View File
@@ -0,0 +1,44 @@
// 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("ImRealnameStatusOptions", "IM实名状态选项", ImRealnameStatusOptions)
dict.RegisterEnums("ImWorkOrderStatusOptions", "IM工单状态选项", ImWorkOrderStatusOptions)
}
const (
ImRealnameStatusPending = "pending"
ImRealnameStatusVerified = "verified"
ImRealnameStatusRejected = "rejected"
)
var ImRealnameStatusOptions = []*model.Option{
dict.GenWarningOption(ImRealnameStatusPending, "待审核"),
dict.GenSuccessOption(ImRealnameStatusVerified, "已通过"),
dict.GenErrorOption(ImRealnameStatusRejected, "已拒绝"),
}
const (
ImWorkOrderStatusReceived = "received"
ImWorkOrderStatusProcessing = "processing"
ImWorkOrderStatusResolved = "resolved"
ImWorkOrderStatusRejected = "rejected"
ImWorkOrderStatusClosed = "closed"
)
var ImWorkOrderStatusOptions = []*model.Option{
dict.GenWarningOption(ImWorkOrderStatusReceived, "待处理"),
dict.GenPrimaryOption(ImWorkOrderStatusProcessing, "处理中"),
dict.GenSuccessOption(ImWorkOrderStatusResolved, "已解决"),
dict.GenErrorOption(ImWorkOrderStatusRejected, "已驳回"),
dict.GenDefaultOption(ImWorkOrderStatusClosed, "已关闭"),
}
+31
View File
@@ -0,0 +1,31 @@
// 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("ImTenantStateOptions", "IM租户状态选项", ImTenantStateOptions)
}
const (
ImTenantStateProvisioning = 0 // 初始化中
ImTenantStateActive = 1 // 正常
ImTenantStateSuspended = 2 // 已停用
ImTenantStateDeleting = 3 // 删除中
ImTenantStateDeleted = 4 // 已删除
)
var ImTenantStateOptions = []*model.Option{
dict.GenWarningOption(ImTenantStateProvisioning, "初始化中"),
dict.GenSuccessOption(ImTenantStateActive, "正常"),
dict.GenErrorOption(ImTenantStateSuspended, "已停用"),
dict.GenWarningOption(ImTenantStateDeleting, "删除中"),
dict.GenDefaultOption(ImTenantStateDeleted, "已删除"),
}
+29
View File
@@ -0,0 +1,29 @@
// 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("ImUserStateOptions", "IM用户状态选项", ImUserStateOptions)
}
const (
ImUserStateOk = 0 // 正常
ImUserStateSuspended = 10 // 已停用
ImUserStateDeleted = 20 // 已删除
ImUserStateUndefined = 30 // 未定义
)
var ImUserStateOptions = []*model.Option{
dict.GenSuccessOption(ImUserStateOk, "正常"),
dict.GenErrorOption(ImUserStateSuspended, "已停用"),
dict.GenDefaultOption(ImUserStateDeleted, "已删除"),
dict.GenWarningOption(ImUserStateUndefined, "未定义"),
}
@@ -7,7 +7,6 @@ package common
import (
"context"
"github.com/gogf/gf/v2/i18n/gi18n"
"hotgo/api/admin/common"
"hotgo/internal/consts"
"hotgo/internal/library/captcha"
@@ -93,7 +92,7 @@ func (c *cSite) LoginConfig(ctx context.Context, _ *common.SiteLoginConfigReq) (
res.LoginConfig = login
res.I18nSwitch = g.Cfg().MustGet(ctx, "system.i18n.switch", true).Bool()
res.DefaultLanguage = g.Cfg().MustGet(ctx, "system.i18n.defaultLanguage", consts.SysDefaultLanguage).String()
res.ProjectName = gi18n.T(ctx, "HotGo管理系统")
res.ProjectName = "IM办公管理系统"
return
}
@@ -0,0 +1,43 @@
// 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/imauditlog"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImAuditLog = cImAuditLog{}
type cImAuditLog struct{}
// List 查看IM审计日志列表
func (c *cImAuditLog) List(ctx context.Context, req *imauditlog.ListReq) (res *imauditlog.ListRes, err error) {
list, totalCount, err := service.SysImAuditLog().List(ctx, &req.ImAuditLogListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImAuditLogListModel{}
}
res = new(imauditlog.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取指定IM审计日志信息
func (c *cImAuditLog) View(ctx context.Context, req *imauditlog.ViewReq) (res *imauditlog.ViewRes, err error) {
data, err := service.SysImAuditLog().View(ctx, &req.ImAuditLogViewInp)
if err != nil {
return
}
res = new(imauditlog.ViewRes)
res.ImAuditLogViewModel = data
return
}
@@ -0,0 +1,151 @@
// 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/imgroup"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImGroup = cImGroup{}
)
type cImGroup struct{}
// List 查看IM群组列表
func (c *cImGroup) List(ctx context.Context, req *imgroup.ListReq) (res *imgroup.ListRes, err error) {
list, totalCount, err := service.SysImGroup().List(ctx, &req.ImGroupListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupListModel{}
}
res = new(imgroup.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取指定IM群组信息
func (c *cImGroup) View(ctx context.Context, req *imgroup.ViewReq) (res *imgroup.ViewRes, err error) {
data, err := service.SysImGroup().View(ctx, &req.ImGroupViewInp)
if err != nil {
return
}
res = new(imgroup.ViewRes)
res.ImGroupViewModel = data
return
}
// MemberList 查看IM群成员列表
func (c *cImGroup) MemberList(ctx context.Context, req *imgroup.MemberListReq) (res *imgroup.MemberListRes, err error) {
list, totalCount, err := service.SysImGroup().MemberList(ctx, &req.ImGroupMemberListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupMemberListModel{}
}
res = new(imgroup.MemberListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// Governance 获取IM群治理配置
func (c *cImGroup) Governance(ctx context.Context, req *imgroup.GovernanceReq) (res *imgroup.GovernanceRes, err error) {
data, err := service.SysImGroup().Governance(ctx, &req.ImGroupTopicInp)
if err != nil {
return
}
res = new(imgroup.GovernanceRes)
res.ImGroupGovernanceModel = data
return
}
// JoinRequestList 查看IM入群申请列表
func (c *cImGroup) JoinRequestList(ctx context.Context, req *imgroup.JoinRequestListReq) (res *imgroup.JoinRequestListRes, err error) {
list, totalCount, err := service.SysImGroup().JoinRequestList(ctx, &req.ImGroupJoinRequestListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupJoinRequestListModel{}
}
res = new(imgroup.JoinRequestListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// InvitationList 查看IM群邀请列表
func (c *cImGroup) InvitationList(ctx context.Context, req *imgroup.InvitationListReq) (res *imgroup.InvitationListRes, err error) {
list, totalCount, err := service.SysImGroup().InvitationList(ctx, &req.ImGroupInvitationListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupInvitationListModel{}
}
res = new(imgroup.InvitationListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// AnnouncementList 查看IM群公告列表
func (c *cImGroup) AnnouncementList(ctx context.Context, req *imgroup.AnnouncementListReq) (res *imgroup.AnnouncementListRes, err error) {
list, totalCount, err := service.SysImGroup().AnnouncementList(ctx, &req.ImGroupAnnouncementListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupAnnouncementListModel{}
}
res = new(imgroup.AnnouncementListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// MuteList 查看IM群禁言列表
func (c *cImGroup) MuteList(ctx context.Context, req *imgroup.MuteListReq) (res *imgroup.MuteListRes, err error) {
list, totalCount, err := service.SysImGroup().MuteList(ctx, &req.ImGroupMuteListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupMuteListModel{}
}
res = new(imgroup.MuteListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// AdminLogList 查看IM群管理日志列表
func (c *cImGroup) AdminLogList(ctx context.Context, req *imgroup.AdminLogListReq) (res *imgroup.AdminLogListRes, err error) {
list, totalCount, err := service.SysImGroup().AdminLogList(ctx, &req.ImGroupAdminLogListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImGroupAdminLogListModel{}
}
res = new(imgroup.AdminLogListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
@@ -0,0 +1,76 @@
package sys
import (
"context"
"hotgo/api/admin/iminvite"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var ImInvite = cImInvite{}
type cImInvite struct{}
func (c *cImInvite) List(ctx context.Context, req *iminvite.ListReq) (res *iminvite.ListRes, err error) {
list, totalCount, err := service.SysImInvite().List(ctx, &req.ImInviteListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImInviteCodeModel{}
}
res = new(iminvite.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
func (c *cImInvite) Create(ctx context.Context, req *iminvite.CreateReq) (res *iminvite.CreateRes, err error) {
data, err := service.SysImInvite().Create(ctx, &req.ImInviteCreateInp)
if err != nil {
return
}
res = new(iminvite.CreateRes)
res.ImInviteCodeModel = data
return
}
func (c *cImInvite) BatchCreate(ctx context.Context, req *iminvite.BatchCreateReq) (res *iminvite.BatchCreateRes, err error) {
data, err := service.SysImInvite().BatchCreate(ctx, &req.ImInviteBatchCreateInp)
if err != nil {
return
}
res = new(iminvite.BatchCreateRes)
res.ImInviteBatchModel = data
return
}
func (c *cImInvite) BatchView(ctx context.Context, req *iminvite.BatchViewReq) (res *iminvite.BatchViewRes, err error) {
data, err := service.SysImInvite().BatchView(ctx, &req.ImInviteBatchViewInp)
if err != nil {
return
}
res = new(iminvite.BatchViewRes)
res.ImInviteBatchDetailModel = data
return
}
func (c *cImInvite) Revoke(ctx context.Context, req *iminvite.RevokeReq) (res *iminvite.RevokeRes, err error) {
err = service.SysImInvite().Revoke(ctx, &req.ImInviteRevokeInp)
return
}
func (c *cImInvite) BatchRevoke(ctx context.Context, req *iminvite.BatchRevokeReq) (res *iminvite.BatchRevokeRes, err error) {
data, err := service.SysImInvite().BatchRevoke(ctx, &req.ImInviteBatchRevokeInp)
if err != nil {
return
}
res = new(iminvite.BatchRevokeRes)
res.ImInviteBatchModel = data
return
}
func (c *cImInvite) Export(ctx context.Context, req *iminvite.ExportReq) (res *iminvite.ExportRes, err error) {
err = service.SysImInvite().Export(ctx, &req.ImInviteListInp)
return
}
@@ -0,0 +1,53 @@
// 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/imrealname"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImRealname = cImRealname{}
)
type cImRealname struct{}
// List 查看IM实名审核列表
func (c *cImRealname) List(ctx context.Context, req *imrealname.ListReq) (res *imrealname.ListRes, err error) {
list, totalCount, err := service.SysImRealname().List(ctx, &req.ImRealnameListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImRealnameListModel{}
}
res = new(imrealname.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取指定IM实名审核信息
func (c *cImRealname) View(ctx context.Context, req *imrealname.ViewReq) (res *imrealname.ViewRes, err error) {
data, err := service.SysImRealname().View(ctx, &req.ImRealnameViewInp)
if err != nil {
return
}
res = new(imrealname.ViewRes)
res.ImRealnameViewModel = data
return
}
// Review 审核IM实名
func (c *cImRealname) Review(ctx context.Context, req *imrealname.ReviewReq) (res *imrealname.ReviewRes, err error) {
err = service.SysImRealname().Review(ctx, &req.ImRealnameReviewInp)
return
}
@@ -0,0 +1,26 @@
package sys
import (
"context"
"hotgo/api/admin/imregistration"
"hotgo/internal/service"
)
var ImRegistration = cImRegistration{}
type cImRegistration struct{}
func (c *cImRegistration) Get(ctx context.Context, req *imregistration.GetReq) (res *imregistration.GetRes, err error) {
data, err := service.SysImRegistration().GetConfig(ctx)
if err != nil {
return
}
res = new(imregistration.GetRes)
res.ImRegistrationConfigModel = data
return
}
func (c *cImRegistration) Update(ctx context.Context, req *imregistration.UpdateReq) (res *imregistration.UpdateRes, err error) {
err = service.SysImRegistration().UpdateConfig(ctx, &req.ImRegistrationConfigUpdateInp)
return
}
@@ -0,0 +1,60 @@
// 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/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImTenant = cImTenant{}
)
type cImTenant struct{}
// List 查看IM租户列表
func (c *cImTenant) List(ctx context.Context, req *imtenant.ListReq) (res *imtenant.ListRes, err error) {
list, totalCount, err := service.SysImTenant().List(ctx, &req.ImTenantListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImTenantListModel{}
}
res = new(imtenant.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// Edit 更新IM租户
func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) {
err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp)
return
}
// View 获取指定IM租户信息
func (c *cImTenant) View(ctx context.Context, req *imtenant.ViewReq) (res *imtenant.ViewRes, err error) {
data, err := service.SysImTenant().View(ctx, &req.ImTenantViewInp)
if err != nil {
return
}
res = new(imtenant.ViewRes)
res.ImTenantViewModel = data
return
}
// Status 更新IM租户状态
func (c *cImTenant) Status(ctx context.Context, req *imtenant.StatusReq) (res *imtenant.StatusRes, err error) {
err = service.SysImTenant().Status(ctx, &req.ImTenantStatusInp)
return
}
@@ -0,0 +1,48 @@
// 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/imuser"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImUser = cImUser{}
)
type cImUser struct{}
// List 查看IM用户列表
func (c *cImUser) List(ctx context.Context, req *imuser.ListReq) (res *imuser.ListRes, err error) {
list, totalCount, err := service.SysImUser().List(ctx, &req.ImUserListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImUserListModel{}
}
res = new(imuser.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取指定IM用户信息
func (c *cImUser) View(ctx context.Context, req *imuser.ViewReq) (res *imuser.ViewRes, err error) {
data, err := service.SysImUser().View(ctx, &req.ImUserViewInp)
if err != nil {
return
}
res = new(imuser.ViewRes)
res.ImUserViewModel = data
return
}
@@ -0,0 +1,53 @@
// 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/imworkorder"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
var (
ImWorkOrder = cImWorkOrder{}
)
type cImWorkOrder struct{}
// List 查看IM举报反馈工单列表
func (c *cImWorkOrder) List(ctx context.Context, req *imworkorder.ListReq) (res *imworkorder.ListRes, err error) {
list, totalCount, err := service.SysImWorkOrder().List(ctx, &req.ImWorkOrderListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImWorkOrderListModel{}
}
res = new(imworkorder.ListRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
// View 获取指定IM举报反馈工单信息
func (c *cImWorkOrder) View(ctx context.Context, req *imworkorder.ViewReq) (res *imworkorder.ViewRes, err error) {
data, err := service.SysImWorkOrder().View(ctx, &req.ImWorkOrderViewInp)
if err != nil {
return
}
res = new(imworkorder.ViewRes)
res.ImWorkOrderViewModel = data
return
}
// Status 更新IM举报反馈工单状态
func (c *cImWorkOrder) Status(ctx context.Context, req *imworkorder.StatusReq) (res *imworkorder.StatusRes, err error) {
err = service.SysImWorkOrder().Status(ctx, &req.ImWorkOrderStatusInp)
return
}
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"hotgo/internal/dao/internal"
)
// imTenantDao is the data access object for the table im_tenant.
// You can define custom methods on it to extend its functionality as needed.
type imTenantDao struct {
*internal.ImTenantDao
}
var (
// ImTenant is a globally accessible object for table im_tenant operations.
ImTenant = imTenantDao{internal.NewImTenantDao()}
)
// Add your custom methods and functionality below.
+22
View File
@@ -0,0 +1,22 @@
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"hotgo/internal/dao/internal"
)
// imTenantConfigDao is the data access object for the table im_tenant_config.
// You can define custom methods on it to extend its functionality as needed.
type imTenantConfigDao struct {
*internal.ImTenantConfigDao
}
var (
// ImTenantConfig is a globally accessible object for table im_tenant_config operations.
ImTenantConfig = imTenantConfigDao{internal.NewImTenantConfigDao()}
)
// Add your custom methods and functionality below.
@@ -22,6 +22,7 @@ type AdminMemberDao struct {
// AdminMemberColumns defines and stores column names for the table hg_admin_member.
type AdminMemberColumns struct {
Id string // 管理员ID
TenantId string // IM租户ID
DeptId string // 部门ID
RoleId string // 角色ID
RealName string // 真实姓名
@@ -54,6 +55,7 @@ type AdminMemberColumns struct {
// adminMemberColumns holds the columns for the table hg_admin_member.
var adminMemberColumns = AdminMemberColumns{
Id: "id",
TenantId: "tenant_id",
DeptId: "dept_id",
RoleId: "role_id",
RealName: "real_name",
+99
View File
@@ -0,0 +1,99 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// ImTenantDao is the data access object for the table im_tenant.
type ImTenantDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns ImTenantColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// ImTenantColumns defines and stores column names for the table im_tenant.
type ImTenantColumns struct {
Id string //
Code string //
Name string //
TenantDesc string //
DbName string //
DbDsn string //
State string //
CreatedAt string //
CreatedBy string //
UpdatedAt string //
UpdatedBy string //
}
// imTenantColumns holds the columns for the table im_tenant.
var imTenantColumns = ImTenantColumns{
Id: "id",
Code: "code",
Name: "name",
TenantDesc: "tenant_desc",
DbName: "db_name",
DbDsn: "db_dsn",
State: "state",
CreatedAt: "created_at",
CreatedBy: "created_by",
UpdatedAt: "updated_at",
UpdatedBy: "updated_by",
}
// NewImTenantDao creates and returns a new DAO object for table data access.
func NewImTenantDao(handlers ...gdb.ModelHandler) *ImTenantDao {
return &ImTenantDao{
group: "default",
table: "im_tenant",
columns: imTenantColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *ImTenantDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *ImTenantDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *ImTenantDao) Columns() ImTenantColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *ImTenantDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *ImTenantDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *ImTenantDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,95 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// ImTenantConfigDao is the data access object for the table im_tenant_config.
type ImTenantConfigDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns ImTenantConfigColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// ImTenantConfigColumns defines and stores column names for the table im_tenant_config.
type ImTenantConfigColumns struct {
Id string //
TenantId string //
MaxUsers string // 0 means unlimited
MaxGroups string // 0 means unlimited
MaxGroupMembers string // 0 means unlimited
CreatedAt string //
CreatedBy string //
UpdatedAt string //
UpdatedBy string //
}
// imTenantConfigColumns holds the columns for the table im_tenant_config.
var imTenantConfigColumns = ImTenantConfigColumns{
Id: "id",
TenantId: "tenant_id",
MaxUsers: "max_users",
MaxGroups: "max_groups",
MaxGroupMembers: "max_group_members",
CreatedAt: "created_at",
CreatedBy: "created_by",
UpdatedAt: "updated_at",
UpdatedBy: "updated_by",
}
// NewImTenantConfigDao creates and returns a new DAO object for table data access.
func NewImTenantConfigDao(handlers ...gdb.ModelHandler) *ImTenantConfigDao {
return &ImTenantConfigDao{
group: "default",
table: "im_tenant_config",
columns: imTenantConfigColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *ImTenantConfigDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *ImTenantConfigDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *ImTenantConfigDao) Columns() ImTenantConfigColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *ImTenantConfigDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *ImTenantConfigDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *ImTenantConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -78,6 +78,15 @@ func GetUserId(ctx context.Context) int64 {
return user.Id
}
// GetTenantId 获取当前身份关联的统一租户ID(im_tenant.id)。
func GetTenantId(ctx context.Context) int64 {
user := GetUser(ctx)
if user == nil {
return 0
}
return user.TenantId
}
// GetRoleId 获取用户角色ID
func GetRoleId(ctx context.Context) int64 {
user := GetUser(ctx)
+268
View File
@@ -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"))
}
}
+37
View File
@@ -0,0 +1,37 @@
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"`
}
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")
}
return Config{
BaseUrl: g.Cfg().MustGet(ctx, "imAdminApi.baseUrl").String(),
ServiceId: g.Cfg().MustGet(ctx, "imAdminApi.serviceId", "hotgo-admin").String(),
ServiceSecret: serviceSecret,
Timeout: timeout,
}
}
+128
View File
@@ -0,0 +1,128 @@
// Package imtenant provides helpers for accessing tenant business databases.
package imtenant
import (
"context"
"fmt"
"hash/fnv"
"hotgo/internal/dao"
"hotgo/internal/model/entity"
"strings"
"sync"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
var tenantDbGroups sync.Map
const registryDbGroup = "imRegistry"
// RegistryDB returns the tenant registry DB. It falls back to the default DB for
// single-database deployments where database.imRegistry is not configured.
func RegistryDB(ctx context.Context) gdb.DB {
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
return g.DB(registryDbGroup)
}
return g.DB()
}
// 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 {
return nil, nil, gerror.New("租户ID不能为空")
}
if err = RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
return nil, nil, gerror.Wrap(err, "获取租户信息失败")
}
if tenant == nil || tenant.Id <= 0 {
return nil, nil, gerror.New("租户不存在")
}
link, err := tenantLink(ctx, tenant)
if err != nil {
return nil, nil, err
}
group := fmt.Sprintf("im_tenant_%d_%x", tenant.Id, hash(link))
if cached, ok := tenantDbGroups.Load(group); ok && cached == link {
db, err = gdb.Instance(group)
return db, tenant, err
}
if err = gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
return nil, nil, gerror.Wrap(err, "初始化租户业务库连接失败")
}
tenantDbGroups.Store(group, link)
db, err = gdb.Instance(group)
if err != nil {
return nil, nil, gerror.Wrap(err, "获取租户业务库连接失败")
}
return db, tenant, nil
}
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
if tenant.DbDsn != "" {
return tenant.DbDsn, nil
}
if tenant.DbName == "" {
return "", gerror.New("租户业务库未配置")
}
defaultLink := defaultDbLink(ctx)
if defaultLink == "" {
return "", gerror.New("默认数据库连接未配置,无法推导租户业务库连接")
}
link, err := replaceMySQLDatabase(defaultLink, tenant.DbName)
if err != nil {
return "", err
}
return link, nil
}
func defaultDbLink(ctx context.Context) string {
if configs, err := gdb.GetConfigGroup("default"); err == nil && len(configs) > 0 {
if configs[0].Link != "" {
return configs[0].Link
}
}
cfg := g.Cfg()
if !cfg.Available(ctx) {
return ""
}
defaultLink := cfg.MustGet(ctx, "database.default.0.link").String()
if defaultLink != "" {
return defaultLink
}
return cfg.MustGet(ctx, "database.default.link").String()
}
func replaceMySQLDatabase(link string, dbName string) (string, error) {
if !strings.HasPrefix(link, "mysql:") {
return "", gerror.New("仅支持从 MySQL 默认连接推导租户业务库连接")
}
dbStart := strings.LastIndex(link, ")/")
if dbStart < 0 {
return "", gerror.New("默认数据库连接格式不正确")
}
dbStart += 2
queryStart := strings.Index(link[dbStart:], "?")
if queryStart < 0 {
return link[:dbStart] + dbName, nil
}
queryStart += dbStart
return link[:dbStart] + dbName + link[queryStart:], nil
}
func hash(s string) uint64 {
h := fnv.New64a()
_, _ = h.Write([]byte(s))
return h.Sum64()
}
+25
View File
@@ -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"
@@ -467,6 +468,30 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
return
}
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
if in.TenantId > 0 {
count, queryErr := imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
Where(dao.ImTenant.Columns().Id, in.TenantId).
Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive).
Count()
if queryErr != nil {
return gerror.Wrap(queryErr, "校验IM租户失败")
}
if count == 0 {
return gerror.New("IM租户不存在或未启用")
}
}
} else if deptType := contexts.GetDeptType(ctx); deptType == consts.DeptTypeTenant ||
deptType == consts.DeptTypeMerchant || deptType == consts.DeptTypeUser {
in.TenantId = contexts.GetTenantId(ctx)
if in.TenantId <= 0 {
return gerror.New("当前管理员未关联租户")
}
} else {
// 非租户域管理员不能通过浏览器为账号指定租户。
in.TenantId = 0
}
cols := dao.AdminMember.Columns()
err = s.VerifyUnique(ctx, &adminin.VerifyUniqueInp{
Id: in.Id,
+2
View File
@@ -228,6 +228,7 @@ func (s *sAdminSite) handleLogin(ctx context.Context, mb *entity.AdminMember) (r
user := &model.Identity{
Id: mb.Id,
TenantId: mb.TenantId,
Pid: mb.Pid,
DeptId: dept.Id,
DeptType: dept.Type,
@@ -319,6 +320,7 @@ func (s *sAdminSite) BindUserContext(ctx context.Context, claims *model.Identity
user := &model.Identity{
Id: mb.Id,
TenantId: mb.TenantId,
Pid: mb.Pid,
DeptId: dept.Id,
DeptType: dept.Type,
@@ -0,0 +1,95 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/imadmin"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/service"
"hotgo/utility/simple"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
)
const (
imAdminOperationAuditTable = "im_admin_operation_audit"
)
func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imadmin.AdminScope, err error) {
operatorId := contexts.GetUserId(ctx)
scope.OperatorId = operatorId
scope.RoleKey = contexts.GetRoleKey(ctx)
scope.RequestId = gctx.CtxId(ctx)
if service.AdminMember().VerifySuperId(ctx, operatorId) {
if selectedTenantId <= 0 {
return scope, gerror.New("请选择IM租户")
}
if err = ensureImTenantActive(ctx, selectedTenantId); err != nil {
return scope, err
}
scope.TenantId = selectedTenantId
return scope, nil
}
if contexts.GetDeptType(ctx) != consts.DeptTypeTenant {
return scope, gerror.New("当前账号不是租户管理员,不能管理邀请码")
}
tenantId := contexts.GetTenantId(ctx)
if tenantId <= 0 {
return scope, gerror.New("当前管理员未关联租户")
}
if err = ensureImTenantActive(ctx, tenantId); err != nil {
return scope, err
}
scope.TenantId = tenantId
return scope, nil
}
func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) {
var tenant *entity.ImTenant
if err = imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
return gerror.Wrap(err, "获取IM租户失败")
}
if tenant == nil || tenant.Id <= 0 {
return gerror.New("IM租户不存在")
}
if tenant.State != consts.ImTenantStateActive {
return gerror.New("IM租户未启用")
}
return nil
}
func writeImAdminAudit(ctx context.Context, tenantId int64, action string, targetType string, targetId int64, result string, detail g.Map) error {
operatorId := contexts.GetUserId(ctx)
request := ghttp.RequestFromCtx(ctx)
ip := ""
if request != nil {
ip = request.GetClientIp()
}
detailJson := gjson.New(detail)
_, err := g.DB().Exec(ctx,
"INSERT INTO "+imAdminOperationAuditTable+" (request_id, tenant_id, operator_id, action, target_type, target_id, result, ip, detail_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)",
gctx.CtxId(ctx),
tenantId,
operatorId,
action,
targetType,
targetId,
result,
ip,
detailJson.String(),
gtime.Now(),
)
if err != nil && !simple.IsDemo(ctx) {
g.Log().Warningf(ctx, "写入IM后台审计失败 action:%s err:%+v", action, err)
}
return err
}
+125
View File
@@ -0,0 +1,125 @@
// 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/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
)
type sSysImAuditLog struct{}
func NewSysImAuditLog() *sSysImAuditLog {
return &sSysImAuditLog{}
}
func init() {
service.RegisterSysImAuditLog(NewSysImAuditLog())
}
// List 获取IM审计日志列表
func (s *sSysImAuditLog) List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := s.model(ctx, db)
mod = s.applyWhere(mod, in)
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM审计日志列表失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImAuditLog) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_audit_logs").Safe().Ctx(ctx).
Fields(
"id",
"actor_uid",
"actor_role",
"action",
"target_type",
"target_id",
"reason",
"request_id",
"ip",
"user_agent",
"CAST(before_snapshot AS CHAR) before_snapshot",
"CAST(after_snapshot AS CHAR) after_snapshot",
"CAST(metadata AS CHAR) metadata",
"created_at",
)
}
func (s *sSysImAuditLog) applyWhere(mod *gdb.Model, in *sysin.ImAuditLogListInp) *gdb.Model {
if in.ActorUid > 0 {
mod = mod.Where("actor_uid", in.ActorUid)
}
if in.ActorRole != "" {
mod = mod.WhereLike("actor_role", "%"+in.ActorRole+"%")
}
if in.Action != "" {
mod = mod.WhereLike("action", "%"+in.Action+"%")
}
if in.TargetType != "" {
mod = mod.Where("target_type", in.TargetType)
}
if in.TargetId != "" {
mod = mod.WhereLike("target_id", "%"+in.TargetId+"%")
}
if in.RequestId != "" {
mod = mod.WhereLike("request_id", "%"+in.RequestId+"%")
}
if in.Ip != "" {
mod = mod.WhereLike("ip", "%"+in.Ip+"%")
}
if in.Keyword != "" {
like := "%" + in.Keyword + "%"
mod = mod.Where("(action LIKE ? OR target_type LIKE ? OR target_id LIKE ? OR reason LIKE ? OR request_id LIKE ?)", like, like, like, like, like)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
}
return mod
}
// View 获取IM审计日志指定信息
func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return nil, err
}
err = s.model(ctx, db).Where("id", in.Id).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
return
}
+456
View File
@@ -0,0 +1,456 @@
// 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/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
)
type sSysImGroup struct{}
func NewSysImGroup() *sSysImGroup {
return &sSysImGroup{}
}
func init() {
service.RegisterSysImGroup(NewSysImGroup())
}
// List 获取IM群组列表
func (s *sSysImGroup) List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := s.groupModel(ctx, db)
mod = s.applyGroupWhere(mod, in)
mod = mod.Page(in.Page, in.PerPage).OrderDesc("t.touchedat").OrderDesc("t.id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群组列表失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImGroup) groupModel(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_topics t").Safe().Ctx(ctx).
LeftJoin("im_group_settings gs", "gs.topic = t.name").
Fields(
"t.name topic",
"t.state",
"COALESCE(gs.owner_user_id, t.owner) owner_id",
"t.seqid seq_id",
"t.delid del_id",
"t.subcnt sub_cnt",
"t.touchedat touched_at",
"t.createdat created_at",
"t.updatedat updated_at",
"t.public public_json",
"t.trusted trusted_json",
"t.tags tags_json",
"t.aux aux_json",
"COALESCE(gs.name, '') group_name",
"COALESCE(gs.avatar_file_id, '') avatar_file_id",
"COALESCE(gs.description, '') description",
"COALESCE(gs.group_status, '') group_status",
"COALESCE(gs.join_approval, 0) join_approval",
"COALESCE(gs.mute_all, 0) mute_all",
"COALESCE(gs.member_upload_files, 0) member_upload_files",
"COALESCE(gs.member_private_chat, 0) member_private_chat",
"COALESCE(gs.member_invite, 0) member_invite",
"COALESCE(gs.max_member_count, 0) max_member_count",
"COALESCE(gs.max_admin_count, 0) max_admin_count",
"COALESCE(gs.member_count, 0) business_member_count",
"COALESCE(gs.version, 0) settings_version",
"gs.updated_at settings_updated_at",
).
WhereLike("t.name", "grp%")
}
func (s *sSysImGroup) applyGroupWhere(mod *gdb.Model, in *sysin.ImGroupListInp) *gdb.Model {
if in.Topic != "" {
mod = mod.WhereLike("t.name", "%"+in.Topic+"%")
}
if in.OwnerId > 0 {
mod = mod.Where("COALESCE(gs.owner_user_id, t.owner)", in.OwnerId)
}
if in.State != nil {
mod = mod.Where("t.state", *in.State)
}
if in.GroupStatus != "" {
mod = mod.Where("gs.group_status", in.GroupStatus)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("t.createdat", in.CreatedAt[0], in.CreatedAt[1])
}
if len(in.TouchedAt) == 2 {
mod = mod.WhereBetween("t.touchedat", in.TouchedAt[0], in.TouchedAt[1])
}
return mod
}
// View 获取IM群组指定信息
func (s *sSysImGroup) View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
err = db.Model("im_topics t").Safe().Ctx(ctx).
LeftJoin("im_group_settings gs", "gs.topic = t.name").
Fields(
"t.name topic",
"t.state",
"COALESCE(gs.owner_user_id, t.owner) owner_id",
"t.seqid seq_id",
"t.delid del_id",
"t.subcnt sub_cnt",
"t.touchedat touched_at",
"t.createdat created_at",
"t.updatedat updated_at",
"t.public public_json",
"t.trusted trusted_json",
"t.tags tags_json",
"t.aux aux_json",
"t.access access_json",
"t.usebt use_bt",
"COALESCE(gs.name, '') group_name",
"COALESCE(gs.avatar_file_id, '') avatar_file_id",
"COALESCE(gs.description, '') description",
"COALESCE(gs.group_status, '') group_status",
"COALESCE(gs.join_approval, 0) join_approval",
"COALESCE(gs.mute_all, 0) mute_all",
"COALESCE(gs.member_upload_files, 0) member_upload_files",
"COALESCE(gs.member_private_chat, 0) member_private_chat",
"COALESCE(gs.member_invite, 0) member_invite",
"COALESCE(gs.max_member_count, 0) max_member_count",
"COALESCE(gs.max_admin_count, 0) max_admin_count",
"COALESCE(gs.member_count, 0) business_member_count",
"COALESCE(gs.version, 0) settings_version",
"gs.updated_at settings_updated_at",
).
Where("t.name", in.Topic).
WhereLike("t.name", "grp%").
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
return
}
// MemberList 获取IM群成员列表
func (s *sSysImGroup) MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
if s.hasGroupMemberTruth(ctx, db, in.Topic) {
return s.memberListFromBusiness(ctx, db, in)
}
return s.memberListFromProjection(ctx, db, in)
}
func (s *sSysImGroup) hasGroupMemberTruth(ctx context.Context, db gdb.DB, topic string) bool {
count, err := db.Model("im_group_members").Safe().Ctx(ctx).Where("topic", topic).Count()
return err == nil && count > 0
}
func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
mod := db.Model("im_group_members gm").Safe().Ctx(ctx).
LeftJoin("im_users u", "u.id = gm.user_id").
LeftJoin("im_user_profiles up", "up.user_id = gm.user_id").
LeftJoin("im_subscriptions s", "s.topic = gm.topic AND s.userid = gm.user_id").
Fields(
"gm.user_id",
"up.nickname",
"u.state",
"s.modewant mode_want",
"s.modegiven mode_given",
"s.recvseqid recv_seq_id",
"s.readseqid read_seq_id",
"s.delid del_id",
"s.deletedat deleted_at",
"s.createdat created_at",
"s.updatedat updated_at",
"s.private private_json",
"gm.role",
"gm.member_status",
"gm.nickname group_nickname",
"gm.pinned",
"gm.mute_notifications",
"gm.join_source",
"gm.joined_by",
"gm.joined_at",
"gm.left_at",
"gm.removed_by",
"gm.version",
).
Where("gm.topic", in.Topic)
if in.UserId > 0 {
mod = mod.Where("gm.user_id", in.UserId)
}
if in.Role != "" {
mod = mod.Where("gm.role", in.Role)
}
if in.MemberStatus != "" {
mod = mod.Where("gm.member_status", in.MemberStatus)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("gm.member_status").OrderAsc("gm.role").OrderAsc("gm.id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!")
return
}
return
}
func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
mod := db.Model("im_subscriptions s").Safe().Ctx(ctx).
LeftJoin("im_users u", "u.id = s.userid").
LeftJoin("im_user_profiles up", "up.user_id = s.userid").
Fields(
"s.userid user_id",
"up.nickname",
"u.state",
"s.modewant mode_want",
"s.modegiven mode_given",
"s.recvseqid recv_seq_id",
"s.readseqid read_seq_id",
"s.delid del_id",
"s.deletedat deleted_at",
"s.createdat created_at",
"s.updatedat updated_at",
"s.private private_json",
"'member' role",
"IF(s.deletedat IS NULL, 'active', 'left') member_status",
"'' group_nickname",
"0 pinned",
"0 mute_notifications",
"'projection' join_source",
"0 joined_by",
"s.createdat joined_at",
"s.deletedat left_at",
"0 removed_by",
"0 version",
).
Where("s.topic", in.Topic)
if in.UserId > 0 {
mod = mod.Where("s.userid", in.UserId)
}
mod = mod.Page(in.Page, in.PerPage).OrderAsc("s.deletedat").OrderAsc("s.userid")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!")
return
}
return
}
// Governance 获取IM群治理配置
func (s *sSysImGroup) Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return nil, err
}
err = db.Model("im_group_settings").Safe().Ctx(ctx).
Fields(
"topic",
"owner_user_id",
"name",
"avatar_file_id",
"description",
"group_status",
"join_approval",
"mute_all",
"member_upload_files",
"member_private_chat",
"member_invite",
"max_member_count",
"max_admin_count",
"member_count",
"version",
"created_by",
"dissolved_by",
"dissolved_at",
"created_at",
"updated_at",
).
Where("topic", in.Topic).
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
return
}
// JoinRequestList 获取IM入群申请列表
func (s *sSysImGroup) JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := db.Model("im_group_join_requests").Safe().Ctx(ctx).Where("topic", in.Topic)
if in.Status != "" {
mod = mod.Where("status", in.Status)
}
if in.ApplicantUserId > 0 {
mod = mod.Where("applicant_user_id", in.ApplicantUserId)
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM入群申请列表失败,请稍后重试!")
}
return
}
// InvitationList 获取IM群邀请列表
func (s *sSysImGroup) InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := db.Model("im_group_invitations").Safe().Ctx(ctx).Where("topic", in.Topic)
if in.Status != "" {
mod = mod.Where("status", in.Status)
}
if in.TargetUserId > 0 {
mod = mod.Where("target_user_id", in.TargetUserId)
}
if in.InviterUserId > 0 {
mod = mod.Where("inviter_user_id", in.InviterUserId)
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群邀请列表失败,请稍后重试!")
}
return
}
// AnnouncementList 获取IM群公告列表
func (s *sSysImGroup) AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := db.Model("im_group_announcements").Safe().Ctx(ctx).Where("topic", in.Topic)
if in.Status != "" {
mod = mod.Where("status", in.Status)
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc("published_at").OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群公告列表失败,请稍后重试!")
}
return
}
// MuteList 获取IM群禁言列表
func (s *sSysImGroup) MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := db.Model("im_group_mutes").Safe().Ctx(ctx).Where("topic", in.Topic)
if in.Status != "" {
mod = mod.Where("status", in.Status)
}
if in.UserId > 0 {
mod = mod.Where("user_id", in.UserId)
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc("muted_at").OrderDesc("user_id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群禁言列表失败,请稍后重试!")
}
return
}
// AdminLogList 获取IM群管理日志列表
func (s *sSysImGroup) AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := db.Model("im_group_admin_logs").Safe().Ctx(ctx).
Fields(
"id",
"log_id",
"topic",
"operator_id",
"target_user_id",
"action",
"object_type",
"object_id",
"CAST(before_data AS CHAR) before_data",
"CAST(after_data AS CHAR) after_data",
"request_id",
"idempotency_key",
"created_at",
).
Where("topic", in.Topic)
if in.Action != "" {
mod = mod.WhereLike("action", "%"+in.Action+"%")
}
if in.OperatorId > 0 {
mod = mod.Where("operator_id", in.OperatorId)
}
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM群管理日志列表失败,请稍后重试!")
}
return
}
+315
View File
@@ -0,0 +1,315 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/library/imadmin"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"io"
"strconv"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
)
type sSysImInvite struct{}
func NewSysImInvite() *sSysImInvite {
return &sSysImInvite{}
}
func init() {
service.RegisterSysImInvite(NewSysImInvite())
}
type imInviteListRequest struct {
Page int `json:"page,omitempty"`
PerPage int `json:"per_page,omitempty"`
InviteCode string `json:"invite_code,omitempty"`
Status string `json:"status,omitempty"`
BatchID int64 `json:"batch_id,omitempty"`
CreatedBy int64 `json:"created_by,omitempty"`
CreatedAtStart string `json:"created_at_start,omitempty"`
CreatedAtEnd string `json:"created_at_end,omitempty"`
ExpiresAtStart string `json:"expires_at_start,omitempty"`
ExpiresAtEnd string `json:"expires_at_end,omitempty"`
}
type imInviteCreateRequest struct {
ExpiresAt string `json:"expires_at"`
Remark string `json:"remark,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
}
type imInviteBatchCreateRequest struct {
Quantity int `json:"quantity"`
ExpiresAt string `json:"expires_at"`
Remark string `json:"remark,omitempty"`
IdempotencyKey string `json:"idempotency_key,omitempty"`
}
type imInviteCodeResponse struct {
ID int64 `json:"id"`
BatchID *int64 `json:"batch_id"`
Code string `json:"code"`
Status string `json:"status"`
ExpiresAt *time.Time `json:"expires_at"`
UsedBy *int64 `json:"used_by"`
UsedAt *time.Time `json:"used_at"`
RequestID string `json:"request_id"`
CreatedBy int64 `json:"created_by"`
Remark string `json:"remark"`
CreatedAt *time.Time `json:"created_at"`
}
type imInviteBatchResponse struct {
BatchID int64 `json:"batch_id"`
Quantity int `json:"quantity"`
ActiveCount int `json:"active_count"`
UsedCount int `json:"used_count"`
RevokedCount int `json:"revoked_count"`
ExpiredCount int `json:"expired_count"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt *time.Time `json:"created_at"`
}
func (s *sSysImInvite) List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
req, err := buildInviteListRequest(in)
if err != nil {
return
}
var page imadmin.Page[imInviteCodeResponse]
if err = imadmin.DefaultClient(ctx).ListCodes(ctx, scope, req, &page); err != nil {
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""})
return
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""})
list = make([]*sysin.ImInviteCodeModel, 0, len(page.Items))
for i := range page.Items {
list = append(list, convertInviteCode(page.Items[i]))
}
totalCount = page.Total
return
}
func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
req := imInviteCreateRequest{ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey}
var upstream imInviteCodeResponse
err = imadmin.DefaultClient(ctx).CreateCode(ctx, scope, req, &upstream)
res = convertInviteCode(upstream)
result := "success"
if err != nil {
result = "failed"
}
targetId, _ := strconv.ParseInt(res.Id, 10, 64)
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", targetId, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark})
return
}
func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
req := imInviteBatchCreateRequest{Quantity: in.Quantity, ExpiresAt: formatRFC3339(in.ExpiresAt), Remark: in.Remark, IdempotencyKey: in.IdempotencyKey}
var upstream imInviteBatchResponse
err = imadmin.DefaultClient(ctx).CreateBatch(ctx, scope, req, &upstream)
res = convertInviteBatch(upstream)
result := "success"
if err != nil {
result = "failed"
}
targetId, _ := strconv.ParseInt(res.BatchId, 10, 64)
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", targetId, result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
return
}
func (s *sSysImInvite) BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) {
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
batchId, err := sysin.ParseOptionalInt64(in.BatchId)
if err != nil {
return
}
var upstream imInviteBatchResponse
err = imadmin.DefaultClient(ctx).GetBatch(ctx, scope, batchId, &upstream)
res = &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(upstream)}
return
}
func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) {
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
id, err := sysin.ParseOptionalInt64(in.Id)
if err != nil {
return
}
err = imadmin.DefaultClient(ctx).RevokeCode(ctx, scope, id)
result := "success"
if err != nil {
result = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, result, nil)
return
}
func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) {
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
batchId, err := sysin.ParseOptionalInt64(in.BatchId)
if err != nil {
return
}
var upstream struct {
Batch *imInviteBatchResponse `json:"batch"`
RevokedCount int `json:"revoked_count"`
}
err = imadmin.DefaultClient(ctx).RevokeBatch(ctx, scope, batchId, &upstream)
if upstream.Batch != nil {
res = convertInviteBatch(*upstream.Batch)
} else {
res = &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(batchId, 10), RevokedCount: upstream.RevokedCount}
}
result := "success"
if err != nil {
result = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", batchId, result, nil)
return
}
func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
req, err := buildInviteListRequest(in)
if err != nil {
return
}
resp, err := imadmin.DefaultClient(ctx).ExportCodes(ctx, scope, req)
if err != nil {
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "failed", g.Map{"hasInviteCode": in.InviteCode != ""})
return
}
defer resp.Body.Close()
request := ghttp.RequestFromCtx(ctx)
if request == nil {
return gerror.New("请求上下文不存在")
}
filename := resp.Header.Get("Content-Disposition")
if filename == "" {
filename = `attachment; filename="registration-invite-codes.csv"`
}
request.Response.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
request.Response.Header().Set("Content-Disposition", filename)
if _, err = io.Copy(request.Response.Writer, resp.Body); err != nil {
return gerror.Wrap(err, "转发导出文件失败")
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""})
fmt.Fprint(request.Response.Writer, "")
return
}
func buildInviteListRequest(in *sysin.ImInviteListInp) (imInviteListRequest, error) {
batchId, err := sysin.ParseOptionalInt64(in.BatchId)
if err != nil {
return imInviteListRequest{}, err
}
createdBy, err := sysin.ParseOptionalInt64(in.CreatedBy)
if err != nil {
return imInviteListRequest{}, err
}
return imInviteListRequest{
Page: in.Page,
PerPage: in.PerPage,
InviteCode: in.InviteCode,
Status: in.Status,
BatchID: batchId,
CreatedBy: createdBy,
CreatedAtStart: formatRFC3339(in.CreatedAtStart),
CreatedAtEnd: formatRFC3339(in.CreatedAtEnd),
ExpiresAtStart: formatRFC3339(in.ExpiresAtStart),
ExpiresAtEnd: formatRFC3339(in.ExpiresAtEnd),
}, nil
}
func formatRFC3339(t *gtime.Time) string {
if t == nil {
return ""
}
return t.Time.UTC().Format(time.RFC3339)
}
func convertTime(t *time.Time) *gtime.Time {
if t == nil {
return nil
}
return gtime.New(t)
}
func formatOptionalInt64(value *int64) string {
if value == nil || *value == 0 {
return ""
}
return strconv.FormatInt(*value, 10)
}
func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel {
return &sysin.ImInviteCodeModel{
Id: strconv.FormatInt(item.ID, 10),
BatchId: formatOptionalInt64(item.BatchID),
Code: item.Code,
Status: item.Status,
ExpiresAt: convertTime(item.ExpiresAt),
UsedBy: formatOptionalInt64(item.UsedBy),
UsedAt: convertTime(item.UsedAt),
RequestId: item.RequestID,
CreatedBy: strconv.FormatInt(item.CreatedBy, 10),
Remark: item.Remark,
CreatedAt: convertTime(item.CreatedAt),
}
}
func convertInviteBatch(item imInviteBatchResponse) *sysin.ImInviteBatchModel {
return &sysin.ImInviteBatchModel{
BatchId: strconv.FormatInt(item.BatchID, 10),
Quantity: item.Quantity,
ActiveCount: item.ActiveCount,
UsedCount: item.UsedCount,
RevokedCount: item.RevokedCount,
ExpiredCount: item.ExpiredCount,
ExpiresAt: convertTime(item.ExpiresAt),
CreatedAt: convertTime(item.CreatedAt),
}
}
+164
View File
@@ -0,0 +1,164 @@
// 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/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
)
type sSysImRealname struct{}
func NewSysImRealname() *sSysImRealname {
return &sSysImRealname{}
}
func init() {
service.RegisterSysImRealname(NewSysImRealname())
}
// List 获取IM实名审核列表
func (s *sSysImRealname) List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := s.model(ctx, db)
mod = s.applyWhere(mod, in)
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM实名审核列表失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImRealname) model(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_realname_verifications").Safe().Ctx(ctx).
Fields(
"id",
"user_id",
"submission_id",
"status",
"id_type",
"masked_name",
"masked_id_number",
"review_reason",
"verified_at",
"created_at",
"updated_at",
)
}
func (s *sSysImRealname) applyWhere(mod *gdb.Model, in *sysin.ImRealnameListInp) *gdb.Model {
if in.UserId > 0 {
mod = mod.Where("user_id", in.UserId)
}
if in.SubmissionId != "" {
mod = mod.WhereLike("submission_id", "%"+in.SubmissionId+"%")
}
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])
}
return mod
}
// View 获取IM实名审核指定信息
func (s *sSysImRealname) View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return nil, err
}
err = db.Model("im_realname_verifications").Safe().Ctx(ctx).
Fields(
"id",
"user_id",
"submission_id",
"status",
"id_type",
"masked_name",
"masked_id_number",
"id_number_hash",
"review_reason",
"verified_at",
"created_at",
"updated_at",
).
Where("id", in.Id).
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
return
}
// Review 审核IM实名
func (s *sSysImRealname) Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
data := g.Map{
"status": in.Status,
"review_reason": in.ReviewReason,
"updated_at": gtime.Now(),
}
if in.Status == consts.ImRealnameStatusVerified {
data["verified_at"] = gtime.Now()
} else {
data["verified_at"] = nil
}
result, err := db.Model("im_realname_verifications").Safe().Ctx(ctx).
Where("id", in.Id).
Where("status", consts.ImRealnameStatusPending).
Data(data).
Update()
if err != nil {
return gerror.Wrap(err, "审核IM实名失败,请稍后重试!")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "读取审核结果失败,请稍后重试!")
}
if affected == 0 {
return gerror.New("当前记录不是待审核状态,不能重复审核")
}
return
}
@@ -0,0 +1,116 @@
package sys
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
)
const (
imRegistrationConfigGroup = "im"
imRegistrationSmsKey = "register_sms_enabled"
)
type sSysImRegistration struct{}
func NewSysImRegistration() *sSysImRegistration {
return &sSysImRegistration{}
}
func init() {
service.RegisterSysImRegistration(NewSysImRegistration())
}
func (s *sSysImRegistration) GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) {
if err = s.verifySuper(ctx); err != nil {
return
}
return s.getConfig(ctx)
}
func (s *sSysImRegistration) UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error) {
if err = s.verifySuper(ctx); err != nil {
return
}
old, err := s.getConfig(ctx)
if err != nil {
return
}
value := "0"
if in.RegisterSmsEnabled {
value = "1"
}
_, err = dao.SysConfig.Ctx(ctx).
Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup).
Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey).
Data(g.Map{
dao.SysConfig.Columns().Value: value,
dao.SysConfig.Columns().UpdatedAt: gtime.Now(),
}).
Update()
if err != nil {
return gerror.Wrap(err, "更新IM注册短信开关失败")
}
_ = writeImAdminAudit(ctx, 0, "registration_sms_config_update", "config", 0, "success", g.Map{
"old": old.RegisterSmsEnabled,
"new": in.RegisterSmsEnabled,
})
return
}
func (s *sSysImRegistration) getConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) {
var record gdb.Record
record, err = dao.SysConfig.Ctx(ctx).
Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup).
Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey).
Where(dao.SysConfig.Columns().Status, 1).
One()
if err != nil {
return nil, gerror.Wrap(err, "获取IM注册短信开关失败")
}
if record.IsEmpty() {
if err = s.seedConfig(ctx); err != nil {
return nil, err
}
return s.getConfig(ctx)
}
return &sysin.ImRegistrationConfigModel{
RegisterSmsEnabled: record[dao.SysConfig.Columns().Value].String() == "1",
UpdatedAt: record[dao.SysConfig.Columns().UpdatedAt].GTime(),
}, nil
}
func (s *sSysImRegistration) seedConfig(ctx context.Context) (err error) {
_, err = dao.SysConfig.Ctx(ctx).Data(g.Map{
dao.SysConfig.Columns().Group: imRegistrationConfigGroup,
dao.SysConfig.Columns().Name: "注册短信发送",
dao.SysConfig.Columns().Type: "bool",
dao.SysConfig.Columns().Key: imRegistrationSmsKey,
dao.SysConfig.Columns().Value: "1",
dao.SysConfig.Columns().DefaultValue: "1",
dao.SysConfig.Columns().Sort: 10,
dao.SysConfig.Columns().Tip: "控制 IM 注册验证码是否真实发送",
dao.SysConfig.Columns().IsDefault: 1,
dao.SysConfig.Columns().Status: 1,
dao.SysConfig.Columns().CreatedAt: gtime.Now(),
dao.SysConfig.Columns().UpdatedAt: gtime.Now(),
}).Insert()
if err != nil {
return gerror.Wrap(err, "初始化IM注册短信开关失败")
}
return
}
func (s *sSysImRegistration) verifySuper(ctx context.Context) error {
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
return gerror.New("仅平台超级管理员可操作IM注册配置")
}
return nil
}
+273
View File
@@ -0,0 +1,273 @@
// 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/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
type sSysImTenant struct{}
func NewSysImTenant() *sSysImTenant {
return &sSysImTenant{}
}
func init() {
service.RegisterSysImTenant(NewSysImTenant())
}
// Model IM租户ORM模型
func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
return handler.Model(imtenant.RegistryDB(ctx).Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
}
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
return imtenant.RegistryDB(ctx).Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
}
// List 获取IM租户列表
func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
mod := s.Model(ctx).Fields(
dao.ImTenant.Columns().Id,
dao.ImTenant.Columns().Code,
dao.ImTenant.Columns().Name,
dao.ImTenant.Columns().TenantDesc,
dao.ImTenant.Columns().DbName,
dao.ImTenant.Columns().State,
dao.ImTenant.Columns().CreatedBy,
dao.ImTenant.Columns().UpdatedBy,
dao.ImTenant.Columns().CreatedAt,
dao.ImTenant.Columns().UpdatedAt,
)
if in.Id > 0 {
mod = mod.Where(dao.ImTenant.Columns().Id, in.Id)
}
if in.Code != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().Code, "%"+in.Code+"%")
}
if in.Name != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
}
if in.DbName != "" {
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
}
if in.State != nil {
mod = mod.Where(dao.ImTenant.Columns().State, *in.State)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
mod = mod.Page(in.Page, in.PerPage).
OrderDesc(dao.ImTenant.Columns().Id).
Hook(hook.MemberSummary)
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!")
return
}
if len(list) == 0 {
return
}
if err = s.fillListConfig(ctx, list); err != nil {
return
}
return
}
func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenantListModel) (err error) {
ids := make([]int64, 0, len(list))
for _, item := range list {
ids = append(ids, item.Id)
}
var configs []*entity.ImTenantConfig
if err = s.configModel(ctx).
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
Scan(&configs); err != nil {
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
}
configMap := make(map[int64]*entity.ImTenantConfig, len(configs))
for _, item := range configs {
configMap[item.TenantId] = item
}
var tenants []*entity.ImTenant
if err = s.Model(ctx).
Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().DbDsn).
WhereIn(dao.ImTenant.Columns().Id, ids).
Scan(&tenants); err != nil {
return gerror.Wrap(err, "获取IM租户连接配置失败,请稍后重试!")
}
dsnMap := make(map[int64]string, len(tenants))
for _, item := range tenants {
dsnMap[item.Id] = item.DbDsn
}
for _, item := range list {
if conf, ok := configMap[item.Id]; ok {
item.MaxUsers = conf.MaxUsers
item.MaxGroups = conf.MaxGroups
item.MaxGroupMembers = conf.MaxGroupMembers
}
item.DbDsnConfigured = dsnMap[item.Id] != ""
}
return
}
// Edit 修改/新增IM租户
func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
return imtenant.RegistryDB(ctx).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
return
}
userId := contexts.GetUserId(ctx)
if in.Id > 0 {
in.UpdatedBy = userId
if _, err = s.Model(ctx).
Fields(sysin.ImTenantUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
}
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
}
in.CreatedBy = userId
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(sysin.ImTenantInsertFields{}).
Data(in).InsertAndGetId()
if err != nil {
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
}
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
})
}
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code)
if id > 0 {
mod = mod.Where(dao.ImTenant.Columns().Id+"<>?", id)
}
count, err := mod.Count()
if err != nil {
return gerror.Wrap(err, "检查IM租户编码失败,请稍后重试!")
}
if count > 0 {
return gerror.New("租户编码已存在")
}
return
}
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, userId int64) (err error) {
var conf *entity.ImTenantConfig
if err = s.configModel(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
Scan(&conf); err != nil {
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
}
data := g.Map{
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
}
if conf != nil && conf.Id > 0 {
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
if _, err = s.configModel(ctx).
Fields(
dao.ImTenantConfig.Columns().MaxUsers,
dao.ImTenantConfig.Columns().MaxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers,
dao.ImTenantConfig.Columns().UpdatedBy,
).
WherePri(conf.Id).Data(data).Update(); err != nil {
return gerror.Wrap(err, "修改IM租户配置失败,请稍后重试!")
}
return
}
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
if _, err = s.configModel(ctx).
Fields(
dao.ImTenantConfig.Columns().TenantId,
dao.ImTenantConfig.Columns().MaxUsers,
dao.ImTenantConfig.Columns().MaxGroups,
dao.ImTenantConfig.Columns().MaxGroupMembers,
dao.ImTenantConfig.Columns().CreatedBy,
).
Data(data).Insert(); err != nil {
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
}
return
}
// View 获取IM租户指定信息
func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
return
}
if res == nil {
return
}
var conf *entity.ImTenantConfig
if err = s.configModel(ctx).
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
Scan(&conf); err != nil {
err = gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
return
}
if conf != nil {
res.MaxUsers = conf.MaxUsers
res.MaxGroups = conf.MaxGroups
res.MaxGroupMembers = conf.MaxGroupMembers
}
res.DbDsnConfigured = res.DbDsn != ""
return
}
// Status 更新IM租户状态
func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.ImTenant.Columns().State: in.State,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新IM租户状态失败,请稍后重试!")
return
}
return
}
+209
View File
@@ -0,0 +1,209 @@
// 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/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
)
type sSysImUser struct{}
func NewSysImUser() *sSysImUser {
return &sSysImUser{}
}
func init() {
service.RegisterSysImUser(NewSysImUser())
}
// List 获取IM用户列表
func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
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 len(list) == 0 {
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
return
}
return
}
func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model {
return db.Model("im_users u").Safe().Ctx(ctx).
LeftJoin("im_user_profiles up", "up.user_id = u.id").
LeftJoin("im_user_security us", "us.user_id = u.id").
Fields(
"u.id user_id",
"u.state",
"u.lastseen last_seen",
"u.useragent user_agent",
"u.createdat created_at",
"u.updatedat updated_at",
"up.nickname",
"up.public_account",
"up.gender",
"up.region",
"us.auth_version",
"us.password_updated_at",
)
}
func (s *sSysImUser) applyListWhere(mod *gdb.Model, in *sysin.ImUserListInp) *gdb.Model {
if in.UserId > 0 {
mod = mod.Where("u.id", in.UserId)
}
if in.Nickname != "" {
mod = mod.WhereLike("up.nickname", "%"+in.Nickname+"%")
}
if in.AuthName != "" {
mod = mod.Where("EXISTS (SELECT 1 FROM im_auth a WHERE a.userid = u.id AND a.uname LIKE ?)", "%"+in.AuthName+"%")
}
if in.State != nil {
mod = mod.Where("u.state", *in.State)
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("u.createdat", in.CreatedAt[0], in.CreatedAt[1])
}
if len(in.LastSeen) == 2 {
mod = mod.WhereBetween("u.lastseen", in.LastSeen[0], in.LastSeen[1])
}
return mod
}
func (s *sSysImUser) fillAuthAndCredentials(ctx context.Context, db gdb.DB, list []*sysin.ImUserListModel) (err error) {
ids := make([]int64, 0, len(list))
userMap := make(map[int64]*sysin.ImUserListModel, len(list))
for _, item := range list {
ids = append(ids, item.UserId)
userMap[item.UserId] = item
}
authRows, err := db.Model("im_auth").Safe().Ctx(ctx).
Fields("userid", "scheme", "uname").
WhereIn("userid", ids).
OrderAsc("scheme").OrderAsc("uname").
All()
if err != nil {
return gerror.Wrap(err, "获取IM用户认证标识失败,请稍后重试!")
}
authMap := make(map[int64][]string)
for _, row := range authRows {
userId := row["userid"].Int64()
scheme := row["scheme"].String()
uname := row["uname"].String()
authMap[userId] = append(authMap[userId], scheme+":"+uname)
}
credentialRows, err := db.Model("im_credentials").Safe().Ctx(ctx).
Fields("userid", "method", "value", "done").
WhereIn("userid", ids).
WhereNull("deletedat").
OrderAsc("method").OrderAsc("value").
All()
if err != nil {
return gerror.Wrap(err, "获取IM用户凭证失败,请稍后重试!")
}
credentialMap := make(map[int64][]string)
for _, row := range credentialRows {
userId := row["userid"].Int64()
method := row["method"].String()
value := row["value"].String()
done := row["done"].Bool()
if !done {
value += "(未验证)"
}
credentialMap[userId] = append(credentialMap[userId], method+":"+value)
}
for userId, item := range userMap {
item.AuthNames = strings.Join(authMap[userId], ", ")
item.Credentials = strings.Join(credentialMap[userId], ", ")
}
return
}
// View 获取IM用户指定信息
func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
fields := append(s.viewFields(), "u.access access_json", "u.public profile_json", "u.trusted trusted_json", "u.tags tags_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", in.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
}
func (s *sSysImUser) viewFields() []interface{} {
return []interface{}{
"u.id user_id",
"u.state",
"u.lastseen last_seen",
"u.useragent user_agent",
"u.createdat created_at",
"u.updatedat updated_at",
"up.nickname",
"up.public_account",
"up.gender",
"up.region",
"up.signature",
"up.avatar_file_id",
"CAST(up.birth_date AS CHAR) birth_date",
"up.version profile_version",
"us.auth_version",
"us.password_updated_at",
}
}
+212
View File
@@ -0,0 +1,212 @@
// 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/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"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/gtime"
)
type sSysImWorkOrder struct{}
func NewSysImWorkOrder() *sSysImWorkOrder {
return &sSysImWorkOrder{}
}
func init() {
service.RegisterSysImWorkOrder(NewSysImWorkOrder())
}
// List 获取IM举报反馈工单列表
func (s *sSysImWorkOrder) List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
mod := s.listModel(ctx, db, in.Type)
mod = s.applyWhere(mod, in)
mod = mod.Page(in.Page, in.PerPage).OrderDesc("created_at").OrderDesc("id")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取IM举报反馈工单列表失败,请稍后重试!")
return
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func (s *sSysImWorkOrder) listModel(ctx context.Context, db gdb.DB, typ string) *gdb.Model {
if typ == sysin.ImWorkOrderTypeAbuse {
return db.Model("im_abuse_reports").Safe().Ctx(ctx).Fields(s.abuseListFields()...)
}
return db.Model("im_feedback_submissions").Safe().Ctx(ctx).Fields(s.feedbackListFields()...)
}
func (s *sSysImWorkOrder) abuseListFields() []interface{} {
return []interface{}{
"'abuse' type",
"id",
"report_id order_id",
"reporter_id user_id",
"target_type",
"CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary",
"reason_code category",
"description content",
"status",
"created_at",
"updated_at",
}
}
func (s *sSysImWorkOrder) feedbackListFields() []interface{} {
return []interface{}{
"'feedback' type",
"id",
"feedback_id order_id",
"user_id",
"'' target_type",
"'' target_summary",
"category",
"content",
"status",
"created_at",
"updated_at",
}
}
func (s *sSysImWorkOrder) applyWhere(mod *gdb.Model, in *sysin.ImWorkOrderListInp) *gdb.Model {
if in.UserId > 0 {
mod = mod.Where("user_id", in.UserId)
}
if in.Status != "" {
mod = mod.Where("status", in.Status)
}
if in.Keyword != "" {
mod = mod.WhereLike("content", "%"+in.Keyword+"%")
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
}
return mod
}
// View 获取IM举报反馈工单指定信息
func (s *sSysImWorkOrder) View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return nil, err
}
if in.Type == sysin.ImWorkOrderTypeAbuse {
res, err = s.viewAbuse(ctx, db, in.Id)
} else {
res, err = s.viewFeedback(ctx, db, in.Id)
}
if err != nil || res == nil {
return
}
res.TenantId = tenant.Id
res.TenantName = tenant.Name
return
}
func (s *sSysImWorkOrder) viewAbuse(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) {
err = db.Model("im_abuse_reports").Safe().Ctx(ctx).
Fields(
"'abuse' type",
"id",
"report_id order_id",
"reporter_id user_id",
"target_type",
"CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary",
"reason_code category",
"description content",
"description",
"evidence_file_ids evidence_json",
"snapshot snapshot_json",
"idempotency_key",
"status",
"created_at",
"updated_at",
).
Where("id", id).
Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取IM举报工单信息失败,请稍后重试!")
}
return
}
func (s *sSysImWorkOrder) viewFeedback(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) {
err = db.Model("im_feedback_submissions").Safe().Ctx(ctx).
Fields(
"'feedback' type",
"id",
"feedback_id order_id",
"user_id",
"'' target_type",
"'' target_summary",
"category",
"content",
"contact",
"attachment_file_ids evidence_json",
"client_context client_context_json",
"include_diagnostics",
"idempotency_key",
"status",
"created_at",
"updated_at",
).
Where("id", id).
Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取IM反馈工单信息失败,请稍后重试!")
}
return
}
// Status 更新IM举报反馈工单状态
func (s *sSysImWorkOrder) Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
db, _, err := imtenant.DB(ctx, in.TenantId)
if err != nil {
return
}
table := "im_abuse_reports"
if in.Type == sysin.ImWorkOrderTypeFeedback {
table = "im_feedback_submissions"
}
if _, err = db.Model(table).Safe().Ctx(ctx).
Where("id", in.Id).
Data(g.Map{
"status": in.Status,
"updated_at": gtime.Now(),
}).
Update(); err != nil {
return gerror.Wrap(err, "更新IM举报反馈工单状态失败,请稍后重试!")
}
return
}
+60 -9
View File
@@ -159,7 +159,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
module = mctx.Module
clientIp = location.GetClientIp(request)
postData = gjson.New(consts.NilJsonToString)
getData = gjson.New(request.URL.Query())
getData = s.MaskSensitiveJson(gjson.New(request.URL.Query()))
headerData = gjson.New(consts.NilJsonToString)
errorData = gjson.New(consts.NilJsonToString)
data entity.SysLog
@@ -194,7 +194,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
// post参数
if body, ok := mctx.Data["request.body"].(*gjson.Json); ok {
postData = body
postData = s.MaskSensitiveJson(body)
}
// post表单
@@ -208,12 +208,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
if postData.IsNil() || len(postData.Map()) == 0 {
postData = gjson.New(consts.NilJsonToString)
} else {
// 隐藏明文显示的密码
for k := range postData.Map() {
if gstr.ContainsI(k, "password") {
postData.MustSet(k, "******")
}
}
postData = s.MaskSensitiveJson(postData)
}
// 当前登录用户
@@ -247,7 +242,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
Url: request.URL.Path,
GetData: getData,
PostData: postData,
HeaderData: s.SimplifyHeaderParams(headerData),
HeaderData: s.SimplifyHeaderParams(s.MaskSensitiveJson(headerData)),
Ip: clientIp,
ProvinceId: ipData.ProvinceCode,
CityId: ipData.CityCode,
@@ -273,6 +268,10 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json {
var filters = []string{"Accept", "Authorization", "Cookie"}
for _, filter := range filters {
v := data.Get(filter)
if isSensitiveLogField(filter) {
data.MustSet(filter, "******")
continue
}
if len(v.String()) > 128 {
data.MustRemove(filter)
}
@@ -280,6 +279,58 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json {
return data
}
// MaskSensitiveJson masks sensitive request fields before access logs are stored.
func (s *sSysLog) MaskSensitiveJson(data *gjson.Json) *gjson.Json {
if data == nil || data.IsNil() {
return data
}
return gjson.New(maskSensitiveValue(data.Map()))
}
func maskSensitiveValue(v interface{}) interface{} {
switch value := v.(type) {
case map[string]interface{}:
res := make(map[string]interface{}, len(value))
for k, item := range value {
if isSensitiveLogField(k) {
res[k] = "******"
continue
}
res[k] = maskSensitiveValue(item)
}
return res
case map[string][]string:
res := make(map[string]interface{}, len(value))
for k, item := range value {
if isSensitiveLogField(k) {
res[k] = "******"
continue
}
res[k] = item
}
return res
case []interface{}:
for i, item := range value {
value[i] = maskSensitiveValue(item)
}
return value
default:
return v
}
}
func isSensitiveLogField(field string) bool {
sensitive := []string{"password", "invitecode", "invite_code", "servicesecret", "service_secret", "authorization", "x-service-secret"}
normalized := gstr.Replace(gstr.Replace(gstr.ToLower(field), "-", ""), "_", "")
for _, item := range sensitive {
item = gstr.Replace(gstr.Replace(item, "-", ""), "_", "")
if normalized == item || gstr.Contains(normalized, item) {
return true
}
}
return false
}
// View 获取指定请求日志信息
func (s *sSysLog) View(ctx context.Context, in *sysin.LogViewInp) (res *sysin.LogViewModel, err error) {
mod := s.Model(ctx)
+1
View File
@@ -22,6 +22,7 @@ type Context struct {
// Identity 通用身份模型
type Identity struct {
Id int64 `json:"id" description:"用户ID"`
TenantId int64 `json:"tenantId" description:"IM租户ID"`
Pid int64 `json:"pid" description:"上级ID"`
DeptId int64 `json:"deptId" description:"部门ID"`
DeptType string `json:"deptType" description:"部门类型"`
+1
View File
@@ -14,6 +14,7 @@ import (
type AdminMember struct {
g.Meta `orm:"table:hg_admin_member, do:true"`
Id any // 管理员ID
TenantId any // IM租户ID
DeptId any // 部门ID
RoleId any // 角色ID
RealName any // 真实姓名
+26
View File
@@ -0,0 +1,26 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// ImTenant is the golang structure of table im_tenant for DAO operations like Where/Data.
type ImTenant struct {
g.Meta `orm:"table:im_tenant, do:true"`
Id any //
Code any //
Name any //
TenantDesc any //
DbName any //
DbDsn any //
State any //
CreatedAt *gtime.Time //
CreatedBy any //
UpdatedAt *gtime.Time //
UpdatedBy any //
}
@@ -0,0 +1,24 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// ImTenantConfig is the golang structure of table im_tenant_config for DAO operations like Where/Data.
type ImTenantConfig struct {
g.Meta `orm:"table:im_tenant_config, do:true"`
Id any //
TenantId any //
MaxUsers any // 0 means unlimited
MaxGroups any // 0 means unlimited
MaxGroupMembers any // 0 means unlimited
CreatedAt *gtime.Time //
CreatedBy any //
UpdatedAt *gtime.Time //
UpdatedBy any //
}
@@ -12,6 +12,7 @@ import (
// AdminMember is the golang structure for table admin_member.
type AdminMember struct {
Id int64 `json:"id" orm:"id" description:"管理员ID"`
TenantId int64 `json:"tenantId" orm:"tenant_id" description:"IM租户ID"`
DeptId int64 `json:"deptId" orm:"dept_id" description:"部门ID"`
RoleId int64 `json:"roleId" orm:"role_id" description:"角色ID"`
RealName string `json:"realName" orm:"real_name" description:"真实姓名"`
+24
View File
@@ -0,0 +1,24 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// ImTenant is the golang structure for table im_tenant.
type ImTenant struct {
Id int64 `json:"id" orm:"id" description:""`
Code string `json:"code" orm:"code" description:""`
Name string `json:"name" orm:"name" description:""`
TenantDesc string `json:"tenantDesc" orm:"tenant_desc" description:""`
DbName string `json:"dbName" orm:"db_name" description:""`
DbDsn string `json:"dbDsn" orm:"db_dsn" description:""`
State int `json:"state" orm:"state" description:""`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""`
UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""`
}
@@ -0,0 +1,22 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// ImTenantConfig is the golang structure for table im_tenant_config.
type ImTenantConfig struct {
Id int64 `json:"id" orm:"id" description:""`
TenantId int64 `json:"tenantId" orm:"tenant_id" description:""`
MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"`
MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"`
MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""`
UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""`
}
@@ -115,6 +115,7 @@ type LoginMemberInfoModel struct {
// MemberEditInp 修改用户
type MemberEditInp struct {
Id int64 `json:"id" dc:"管理员ID"`
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
RoleId int64 `json:"roleId" v:"required#角色不能为空" dc:"角色ID"`
PostIds []int64 `json:"postIds" dc:"岗位ID"`
DeptId int64 `json:"deptId" v:"required#部门不能为空" dc:"部门ID"`
@@ -0,0 +1,75 @@
// 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"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
// ImAuditLogListInp 获取IM审计日志列表
type ImAuditLogListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
ActorUid int64 `json:"actorUid" dc:"操作人UID"`
ActorRole string `json:"actorRole" dc:"操作角色"`
Action string `json:"action" dc:"操作类型"`
TargetType string `json:"targetType" dc:"目标类型"`
TargetId string `json:"targetId" dc:"目标ID"`
RequestId string `json:"requestId" dc:"请求ID"`
Ip string `json:"ip" dc:"IP"`
Keyword string `json:"keyword" dc:"关键词"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
return
}
type ImAuditLogListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
ActorUid int64 `json:"actorUid" dc:"操作人UID"`
ActorRole string `json:"actorRole" dc:"操作角色"`
Action string `json:"action" dc:"操作类型"`
TargetType string `json:"targetType" dc:"目标类型"`
TargetId string `json:"targetId" dc:"目标ID"`
Reason string `json:"reason" dc:"操作原因"`
RequestId string `json:"requestId" dc:"请求ID"`
Ip string `json:"ip" dc:"IP"`
UserAgent string `json:"userAgent" dc:"客户端"`
BeforeSnapshot string `json:"beforeSnapshot" dc:"变更前快照"`
AfterSnapshot string `json:"afterSnapshot" dc:"变更后快照"`
Metadata string `json:"metadata" dc:"元数据"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
// ImAuditLogViewInp 获取指定IM审计日志信息
type ImAuditLogViewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
type ImAuditLogViewModel struct {
ImAuditLogListModel
}
@@ -0,0 +1,290 @@
// 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"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
// ImGroupListInp 获取IM群组列表
type ImGroupListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Topic string `json:"topic" dc:"Topic"`
OwnerId int64 `json:"ownerId" dc:"群主ID"`
State *int `json:"state" dc:"通信状态"`
GroupStatus string `json:"groupStatus" dc:"业务状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
TouchedAt []*gtime.Time `json:"touchedAt" dc:"活跃时间"`
}
func (in *ImGroupListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
return
}
type ImGroupListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Topic string `json:"topic" dc:"Topic"`
State int `json:"state" dc:"状态"`
OwnerId int64 `json:"ownerId" dc:"群主ID"`
SeqId int `json:"seqId" dc:"最新消息序号"`
DelId int `json:"delId" dc:"删除序号"`
SubCnt int `json:"subCnt" dc:"成员数"`
TouchedAt *gtime.Time `json:"touchedAt" dc:"活跃时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
PublicJson string `json:"publicJson" dc:"公开资料"`
TrustedJson string `json:"trustedJson" dc:"可信资料"`
TagsJson string `json:"tagsJson" dc:"标签"`
AuxJson string `json:"auxJson" dc:"辅助资料"`
GroupName string `json:"groupName" dc:"群名称"`
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
Description string `json:"description" dc:"简介"`
GroupStatus string `json:"groupStatus" dc:"业务状态"`
JoinApproval int `json:"joinApproval" dc:"入群审批"`
MuteAll int `json:"muteAll" dc:"全员禁言"`
MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"`
MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"`
MemberInvite int `json:"memberInvite" dc:"成员邀请"`
MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"`
MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"`
BusinessMemberCount int `json:"businessMemberCount" dc:"业务成员数"`
SettingsVersion int `json:"settingsVersion" dc:"治理配置版本"`
SettingsUpdatedAt *gtime.Time `json:"settingsUpdatedAt" dc:"治理配置更新时间"`
}
// ImGroupViewInp 获取指定IM群组信息
type ImGroupViewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
}
func (in *ImGroupViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Topic == "" {
return gerror.New("Topic不能为空")
}
return
}
type ImGroupViewModel struct {
ImGroupListModel
AccessJson string `json:"accessJson" dc:"访问权限"`
UseBt int `json:"useBt" dc:"是否开启BT"`
}
// ImGroupMemberListInp 获取IM群成员列表
type ImGroupMemberListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
UserId int64 `json:"userId" dc:"用户ID"`
Role string `json:"role" dc:"角色"`
MemberStatus string `json:"memberStatus" dc:"成员状态"`
}
func (in *ImGroupMemberListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Topic == "" {
return gerror.New("Topic不能为空")
}
return
}
type ImGroupMemberListModel struct {
UserId int64 `json:"userId" dc:"用户ID"`
Nickname string `json:"nickname" dc:"昵称"`
State int `json:"state" dc:"用户状态"`
ModeWant string `json:"modeWant" dc:"期望权限"`
ModeGiven string `json:"modeGiven" dc:"授予权限"`
RecvSeqId int `json:"recvSeqId" dc:"接收序号"`
ReadSeqId int `json:"readSeqId" dc:"已读序号"`
DelId int `json:"delId" dc:"删除序号"`
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"加入时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
PrivateJson string `json:"privateJson" dc:"私有资料"`
Role string `json:"role" dc:"业务角色"`
MemberStatus string `json:"memberStatus" dc:"业务状态"`
GroupNickname string `json:"groupNickname" dc:"群昵称"`
Pinned int `json:"pinned" dc:"置顶"`
MuteNotifications int `json:"muteNotifications" dc:"免打扰"`
JoinSource string `json:"joinSource" dc:"加入来源"`
JoinedBy int64 `json:"joinedBy" dc:"邀请/处理人"`
JoinedAt *gtime.Time `json:"joinedAt" dc:"业务加入时间"`
LeftAt *gtime.Time `json:"leftAt" dc:"离开时间"`
RemovedBy int64 `json:"removedBy" dc:"移除人"`
Version int `json:"version" dc:"版本"`
}
// ImGroupTopicInp 群Topic通用入参
type ImGroupTopicInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
}
func (in *ImGroupTopicInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Topic == "" {
return gerror.New("Topic不能为空")
}
return
}
type ImGroupGovernanceModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Topic string `json:"topic" dc:"Topic"`
OwnerUserId int64 `json:"ownerUserId" dc:"群主ID"`
Name string `json:"name" dc:"群名称"`
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
Description string `json:"description" dc:"简介"`
GroupStatus string `json:"groupStatus" dc:"业务状态"`
JoinApproval int `json:"joinApproval" dc:"入群审批"`
MuteAll int `json:"muteAll" dc:"全员禁言"`
MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"`
MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"`
MemberInvite int `json:"memberInvite" dc:"成员邀请"`
MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"`
MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"`
MemberCount int `json:"memberCount" dc:"成员数"`
Version int `json:"version" dc:"版本"`
CreatedBy int64 `json:"createdBy" dc:"创建人"`
DissolvedBy int64 `json:"dissolvedBy" dc:"解散人"`
DissolvedAt *gtime.Time `json:"dissolvedAt" dc:"解散时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
type ImGroupJoinRequestListInp struct {
form.PageReq
ImGroupTopicInp
Status string `json:"status" dc:"状态"`
ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"`
}
type ImGroupJoinRequestListModel struct {
Id int64 `json:"id" dc:"ID"`
RequestId string `json:"requestId" dc:"申请ID"`
Topic string `json:"topic" dc:"Topic"`
ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"`
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
Source string `json:"source" dc:"来源"`
Message string `json:"message" dc:"申请消息"`
Status string `json:"status" dc:"状态"`
RejectReason string `json:"rejectReason" dc:"拒绝原因"`
HandledBy int64 `json:"handledBy" dc:"处理人"`
HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
Version int `json:"version" dc:"版本"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
type ImGroupInvitationListInp struct {
form.PageReq
ImGroupTopicInp
Status string `json:"status" dc:"状态"`
TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"`
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
}
type ImGroupInvitationListModel struct {
Id int64 `json:"id" dc:"ID"`
InvitationId string `json:"invitationId" dc:"邀请ID"`
Topic string `json:"topic" dc:"Topic"`
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"`
Message string `json:"message" dc:"邀请消息"`
Status string `json:"status" dc:"状态"`
Result string `json:"result" dc:"结果"`
JoinRequestId string `json:"joinRequestId" dc:"入群申请ID"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
type ImGroupAnnouncementListInp struct {
form.PageReq
ImGroupTopicInp
Status string `json:"status" dc:"状态"`
}
type ImGroupAnnouncementListModel struct {
Id int64 `json:"id" dc:"ID"`
AnnouncementId string `json:"announcementId" dc:"公告ID"`
Topic string `json:"topic" dc:"Topic"`
AuthorUserId int64 `json:"authorUserId" dc:"发布人ID"`
Content string `json:"content" dc:"内容"`
Status string `json:"status" dc:"状态"`
Version int `json:"version" dc:"版本"`
PublishedAt *gtime.Time `json:"publishedAt" dc:"发布时间"`
DeletedBy int64 `json:"deletedBy" dc:"删除人"`
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
type ImGroupMuteListInp struct {
form.PageReq
ImGroupTopicInp
Status string `json:"status" dc:"状态"`
UserId int64 `json:"userId" dc:"用户ID"`
}
type ImGroupMuteListModel struct {
Topic string `json:"topic" dc:"Topic"`
UserId int64 `json:"userId" dc:"用户ID"`
MutedBy int64 `json:"mutedBy" dc:"禁言人"`
MutedAt *gtime.Time `json:"mutedAt" dc:"禁言时间"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
Reason string `json:"reason" dc:"原因"`
Status string `json:"status" dc:"状态"`
RevokedBy int64 `json:"revokedBy" dc:"解除人"`
RevokedAt *gtime.Time `json:"revokedAt" dc:"解除时间"`
Version int `json:"version" dc:"版本"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
type ImGroupAdminLogListInp struct {
form.PageReq
ImGroupTopicInp
Action string `json:"action" dc:"动作"`
OperatorId int64 `json:"operatorId" dc:"操作人ID"`
}
type ImGroupAdminLogListModel struct {
Id int64 `json:"id" dc:"ID"`
LogId string `json:"logId" dc:"日志ID"`
Topic string `json:"topic" dc:"Topic"`
OperatorId int64 `json:"operatorId" dc:"操作人"`
TargetUserId int64 `json:"targetUserId" dc:"目标用户"`
Action string `json:"action" dc:"动作"`
ObjectType string `json:"objectType" dc:"对象类型"`
ObjectId string `json:"objectId" dc:"对象ID"`
BeforeData string `json:"beforeData" dc:"变更前"`
AfterData string `json:"afterData" dc:"变更后"`
RequestId string `json:"requestId" dc:"请求ID"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
@@ -0,0 +1,140 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"strconv"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
type ImInviteListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
InviteCode string `json:"inviteCode" dc:"完整邀请码"`
Status string `json:"status" dc:"状态"`
BatchId string `json:"batchId" dc:"批次ID"`
CreatedBy string `json:"createdBy" dc:"创建人"`
CreatedAtStart *gtime.Time `json:"createdAtStart" dc:"创建开始时间"`
CreatedAtEnd *gtime.Time `json:"createdAtEnd" dc:"创建结束时间"`
ExpiresAtStart *gtime.Time `json:"expiresAtStart" dc:"过期开始时间"`
ExpiresAtEnd *gtime.Time `json:"expiresAtEnd" dc:"过期结束时间"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
ExpiresAt []*gtime.Time `json:"expiresAt" dc:"到期时间"`
}
func (in *ImInviteListInp) Filter(ctx context.Context) (err error) {
if in.PerPage <= 0 {
in.PerPage = 20
}
if in.PerPage > 100 {
return gerror.New("每页数量最大值不能大于100")
}
if in.InviteCode != "" && len(in.InviteCode) != 5 {
return gerror.New("邀请码必须为完整5位")
}
if len(in.CreatedAt) == 2 {
in.CreatedAtStart = in.CreatedAt[0]
in.CreatedAtEnd = in.CreatedAt[1]
}
if len(in.ExpiresAt) == 2 {
in.ExpiresAtStart = in.ExpiresAt[0]
in.ExpiresAtEnd = in.ExpiresAt[1]
}
return
}
type ImInviteCreateInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
Remark string `json:"remark" dc:"备注"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
func (in *ImInviteCreateInp) Filter(ctx context.Context) (err error) {
if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) {
return gerror.New("到期时间必须晚于当前时间")
}
if len(in.Remark) > 255 {
return gerror.New("备注不能超过255个字符")
}
return
}
type ImInviteBatchCreateInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
Quantity int `json:"quantity" dc:"数量"`
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
Remark string `json:"remark" dc:"备注"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
if in.Quantity < 1 || in.Quantity > 1000 {
return gerror.New("批量生成数量必须在1到1000之间")
}
if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) {
return gerror.New("到期时间必须晚于当前时间")
}
if len(in.Remark) > 255 {
return gerror.New("备注不能超过255个字符")
}
return
}
type ImInviteBatchViewInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"`
}
type ImInviteRevokeInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
Id string `json:"id" v:"required#邀请码ID不能为空" dc:"邀请码ID"`
}
type ImInviteBatchRevokeInp struct {
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"`
}
type ImInviteCodeModel struct {
Id string `json:"id" dc:"ID"`
BatchId string `json:"batchId" dc:"批次ID"`
Code string `json:"code" dc:"邀请码"`
Status string `json:"status" dc:"状态"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
UsedBy string `json:"usedBy" dc:"使用用户"`
UsedAt *gtime.Time `json:"usedAt" dc:"使用时间"`
RequestId string `json:"requestId" dc:"请求ID"`
CreatedBy string `json:"createdBy" dc:"创建人"`
Remark string `json:"remark" dc:"备注"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
type ImInviteBatchModel struct {
BatchId string `json:"batchId" dc:"批次ID"`
Quantity int `json:"quantity" dc:"数量"`
ActiveCount int `json:"activeCount" dc:"有效数量"`
UsedCount int `json:"usedCount" dc:"已使用数量"`
RevokedCount int `json:"revokedCount" dc:"已撤销数量"`
ExpiredCount int `json:"expiredCount" dc:"已过期数量"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
type ImInviteBatchDetailModel struct {
ImInviteBatchModel
List []*ImInviteCodeModel `json:"list" dc:"批次邀请码"`
}
func ParseOptionalInt64(value string) (int64, error) {
if value == "" {
return 0, nil
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed <= 0 {
return 0, gerror.New("ID格式不正确")
}
return parsed, nil
}
@@ -0,0 +1,96 @@
// 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/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
// ImRealnameListInp 获取IM实名审核列表
type ImRealnameListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId int64 `json:"userId" dc:"用户ID"`
SubmissionId string `json:"submissionId" dc:"提交ID"`
Status string `json:"status" dc:"状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *ImRealnameListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
return
}
type ImRealnameListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Id int64 `json:"id" dc:"ID"`
UserId int64 `json:"userId" dc:"用户ID"`
SubmissionId string `json:"submissionId" dc:"提交ID"`
Status string `json:"status" dc:"状态"`
MaskedName string `json:"maskedName" dc:"脱敏姓名"`
MaskedIdNumber string `json:"maskedIdNumber" dc:"脱敏证件号"`
IdType string `json:"idType" dc:"证件类型"`
ReviewReason string `json:"reviewReason" dc:"审核原因"`
VerifiedAt *gtime.Time `json:"verifiedAt" dc:"通过时间"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
// ImRealnameViewInp 获取指定IM实名审核信息
type ImRealnameViewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImRealnameViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
type ImRealnameViewModel struct {
ImRealnameListModel
IdNumberHash string `json:"idNumberHash" dc:"证件号哈希"`
}
// ImRealnameReviewInp 审核IM实名
type ImRealnameReviewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
Status string `json:"status" dc:"审核状态"`
ReviewReason string `json:"reviewReason" dc:"审核原因"`
}
func (in *ImRealnameReviewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
if !dict.HasOptionKey(consts.ImRealnameStatusOptions, in.Status) {
return gerror.New("审核状态不正确")
}
if in.Status == consts.ImRealnameStatusRejected && in.ReviewReason == "" {
return gerror.New("拒绝原因不能为空")
}
return
}
type ImRealnameReviewModel struct{}
@@ -0,0 +1,12 @@
package sysin
import "github.com/gogf/gf/v2/os/gtime"
type ImRegistrationConfigModel struct {
RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
}
type ImRegistrationConfigUpdateInp struct {
RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"`
}
@@ -0,0 +1,157 @@
// 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/consts"
"hotgo/internal/library/dict"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// ImTenantUpdateFields 修改IM租户字段过滤
type ImTenantUpdateFields struct {
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
DbName string `json:"dbName" dc:"业务库名"`
DbDsn string `json:"dbDsn" dc:"独立连接串"`
State int `json:"state" dc:"状态"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
}
// ImTenantInsertFields 新增IM租户字段过滤
type ImTenantInsertFields struct {
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
DbName string `json:"dbName" dc:"业务库名"`
DbDsn string `json:"dbDsn" dc:"独立连接串"`
State int `json:"state" dc:"状态"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
}
// ImTenantConfigUpdateFields 修改IM租户配置字段过滤
type ImTenantConfigUpdateFields struct {
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
}
// ImTenantConfigInsertFields 新增IM租户配置字段过滤
type ImTenantConfigInsertFields struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
}
// ImTenantEditInp 修改/新增IM租户
type ImTenantEditInp struct {
entity.ImTenant
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
}
func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
if e := g.Validator().Rules("required|length:2,64").Data(in.Code).Messages("租户编码不能为空|租户编码长度应为2到64个字符").Run(ctx); e != nil {
return e.Current()
}
if e := g.Validator().Rules("required|max-length:128").Data(in.Name).Messages("租户名称不能为空|租户名称不能超过128个字符").Run(ctx); e != nil {
return e.Current()
}
if in.DbName == "" && in.DbDsn == "" {
return gerror.New("业务库名和独立连接串至少填写一个")
}
if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) {
return gerror.New("租户状态不正确")
}
return
}
type ImTenantEditModel struct{}
// ImTenantViewInp 获取指定IM租户信息
type ImTenantViewInp struct {
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) {
return
}
type ImTenantViewModel struct {
entity.ImTenant
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
}
// ImTenantListInp 获取IM租户列表
type ImTenantListInp struct {
form.PageReq
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
DbName string `json:"dbName" dc:"业务库名"`
State *int `json:"state" dc:"状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *ImTenantListInp) Filter(ctx context.Context) (err error) {
return
}
type ImTenantListModel struct {
Id int64 `json:"id" dc:"ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
DbName string `json:"dbName" dc:"业务库名"`
State int `json:"state" dc:"状态"`
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
CreatedBy int64 `json:"createdBy" dc:"创建者"`
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
// ImTenantStatusInp 更新IM租户状态
type ImTenantStatusInp struct {
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
State int `json:"state" dc:"状态"`
}
func (in *ImTenantStatusInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) {
return gerror.New("租户状态不正确")
}
return
}
type ImTenantStatusModel struct{}
@@ -0,0 +1,80 @@
// 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"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
// ImUserListInp 获取IM用户列表
type ImUserListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId int64 `json:"userId" dc:"用户ID"`
Nickname string `json:"nickname" dc:"昵称"`
AuthName string `json:"authName" dc:"登录标识"`
State *int `json:"state" dc:"账号状态"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
LastSeen []*gtime.Time `json:"lastSeen" dc:"最后在线时间"`
}
func (in *ImUserListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
return
}
type ImUserListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
UserId int64 `json:"userId" dc:"用户ID"`
Nickname string `json:"nickname" dc:"昵称"`
PublicAccount string `json:"publicAccount" dc:"公开账号"`
Gender string `json:"gender" dc:"性别"`
Region string `json:"region" dc:"地区"`
State int `json:"state" dc:"账号状态"`
AuthVersion int `json:"authVersion" dc:"认证版本"`
PasswordUpdatedAt *gtime.Time `json:"passwordUpdatedAt" dc:"密码更新时间"`
AuthNames string `json:"authNames" dc:"登录标识"`
Credentials string `json:"credentials" dc:"凭证"`
LastSeen *gtime.Time `json:"lastSeen" dc:"最后在线时间"`
UserAgent string `json:"userAgent" dc:"客户端"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
// ImUserViewInp 获取指定IM用户信息
type ImUserViewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
UserId int64 `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
func (in *ImUserViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.UserId <= 0 {
return gerror.New("用户ID不能为空")
}
return
}
type ImUserViewModel struct {
ImUserListModel
Signature string `json:"signature" dc:"个性签名"`
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
BirthDate string `json:"birthDate" dc:"出生日期"`
ProfileJson string `json:"profileJson" dc:"Tinode Public资料"`
TrustedJson string `json:"trustedJson" dc:"Tinode Trusted资料"`
TagsJson string `json:"tagsJson" dc:"Tinode标签"`
AccessJson string `json:"accessJson" dc:"访问权限"`
ProfileVersion int `json:"profileVersion" dc:"资料版本"`
}
@@ -0,0 +1,118 @@
// 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/consts"
"hotgo/internal/library/dict"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
)
const (
ImWorkOrderTypeAbuse = "abuse"
ImWorkOrderTypeFeedback = "feedback"
)
// ImWorkOrderListInp 获取IM举报反馈工单列表
type ImWorkOrderListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
Type string `json:"type" dc:"工单类型"`
UserId int64 `json:"userId" dc:"提交人ID"`
Status string `json:"status" dc:"状态"`
Keyword string `json:"keyword" dc:"关键词"`
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
}
func (in *ImWorkOrderListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Type == "" {
return gerror.New("请选择工单类型")
}
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
return gerror.New("工单类型不正确")
}
return
}
type ImWorkOrderListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
Type string `json:"type" dc:"工单类型"`
Id int64 `json:"id" dc:"ID"`
OrderId string `json:"orderId" dc:"工单ID"`
UserId int64 `json:"userId" dc:"提交人ID"`
TargetType string `json:"targetType" dc:"目标类型"`
TargetSummary string `json:"targetSummary" dc:"目标摘要"`
Category string `json:"category" dc:"分类"`
Content string `json:"content" dc:"内容"`
Status string `json:"status" dc:"状态"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
}
// ImWorkOrderViewInp 获取指定IM举报反馈工单信息
type ImWorkOrderViewInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"`
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImWorkOrderViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
return gerror.New("工单类型不正确")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
type ImWorkOrderViewModel struct {
ImWorkOrderListModel
Description string `json:"description" dc:"描述"`
Contact string `json:"contact" dc:"联系方式"`
EvidenceJson string `json:"evidenceJson" dc:"证据/附件"`
SnapshotJson string `json:"snapshotJson" dc:"快照"`
ClientContextJson string `json:"clientContextJson" dc:"客户端上下文"`
IncludeDiagnostics int `json:"includeDiagnostics" dc:"是否包含诊断"`
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
}
// ImWorkOrderStatusInp 更新IM举报反馈工单状态
type ImWorkOrderStatusInp struct {
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"`
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
Status string `json:"status" dc:"状态"`
}
func (in *ImWorkOrderStatusInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
return gerror.New("工单类型不正确")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
if !dict.HasOptionKey(consts.ImWorkOrderStatusOptions, in.Status) {
return gerror.New("工单状态不正确")
}
return
}
type ImWorkOrderStatusModel struct{}
@@ -0,0 +1,12 @@
// Package genrouter
// @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 genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAuditLog) // IM审计日志
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImGroup) // IM群组
}
@@ -0,0 +1,8 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImInvite) // IM注册邀请码
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRealname) // IM实名审核
}
@@ -0,0 +1,8 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRegistration) // IM注册配置
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImTenant) // IM租户
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImUser) // IM用户
}
@@ -0,0 +1,7 @@
package genrouter
import "hotgo/internal/controller/admin/sys"
func init() {
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImWorkOrder) // IM举报反馈
}
+178
View File
@@ -171,6 +171,88 @@ type (
// Switch 更新CURD列表开关
Switch(ctx context.Context, in *sysin.CurdDemoSwitchInp) (err error)
}
ISysImTenant interface {
// Model IM租户ORM模型
Model(ctx context.Context, option ...*handler.Option) *gdb.Model
// List 获取IM租户列表
List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error)
// Edit 修改/新增IM租户
Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error)
// View 获取IM租户指定信息
View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error)
// Status 更新IM租户状态
Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error)
}
ISysImRegistration interface {
// GetConfig 获取IM注册配置
GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error)
// UpdateConfig 更新IM注册配置
UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error)
}
ISysImInvite interface {
// List 查询IM注册邀请码
List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error)
// Create 单个生成IM注册邀请码
Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error)
// BatchCreate 批量生成IM注册邀请码
BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error)
// BatchView 查看批次详情
BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error)
// Revoke 撤销邀请码
Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error)
// BatchRevoke 批量撤销邀请码
BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error)
// Export 导出邀请码
Export(ctx context.Context, in *sysin.ImInviteListInp) (err error)
}
ISysImUser interface {
// List 获取IM用户列表
List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error)
// View 获取IM用户指定信息
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
}
ISysImGroup interface {
// List 获取IM群组列表
List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error)
// View 获取IM群组指定信息
View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error)
// MemberList 获取IM群成员列表
MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error)
// Governance 获取IM群治理配置
Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error)
// JoinRequestList 获取IM入群申请列表
JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error)
// InvitationList 获取IM群邀请列表
InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error)
// AnnouncementList 获取IM群公告列表
AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error)
// MuteList 获取IM群禁言列表
MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error)
// AdminLogList 获取IM群管理日志列表
AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error)
}
ISysImRealname interface {
// List 获取IM实名审核列表
List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error)
// View 获取IM实名审核指定信息
View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error)
// Review 审核IM实名
Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error)
}
ISysImWorkOrder interface {
// List 获取IM举报反馈工单列表
List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error)
// View 获取IM举报反馈工单指定信息
View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error)
// Status 更新IM举报反馈工单状态
Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error)
}
ISysImAuditLog interface {
// List 获取IM审计日志列表
List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error)
// View 获取IM审计日志指定信息
View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error)
}
ISysDictData interface {
// Delete 删除
Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error
@@ -414,6 +496,14 @@ var (
localSysCron ISysCron
localSysCronGroup ISysCronGroup
localSysCurdDemo ISysCurdDemo
localSysImTenant ISysImTenant
localSysImRegistration ISysImRegistration
localSysImInvite ISysImInvite
localSysImUser ISysImUser
localSysImGroup ISysImGroup
localSysImRealname ISysImRealname
localSysImWorkOrder ISysImWorkOrder
localSysImAuditLog ISysImAuditLog
localSysDictData ISysDictData
localSysDictType ISysDictType
localSysEmsLog ISysEmsLog
@@ -517,6 +607,94 @@ func RegisterSysCurdDemo(i ISysCurdDemo) {
localSysCurdDemo = i
}
func SysImTenant() ISysImTenant {
if localSysImTenant == nil {
panic("implement not found for interface ISysImTenant, forgot register?")
}
return localSysImTenant
}
func RegisterSysImTenant(i ISysImTenant) {
localSysImTenant = i
}
func SysImRegistration() ISysImRegistration {
if localSysImRegistration == nil {
panic("implement not found for interface ISysImRegistration, forgot register?")
}
return localSysImRegistration
}
func RegisterSysImRegistration(i ISysImRegistration) {
localSysImRegistration = i
}
func SysImInvite() ISysImInvite {
if localSysImInvite == nil {
panic("implement not found for interface ISysImInvite, forgot register?")
}
return localSysImInvite
}
func RegisterSysImInvite(i ISysImInvite) {
localSysImInvite = i
}
func SysImUser() ISysImUser {
if localSysImUser == nil {
panic("implement not found for interface ISysImUser, forgot register?")
}
return localSysImUser
}
func RegisterSysImUser(i ISysImUser) {
localSysImUser = i
}
func SysImGroup() ISysImGroup {
if localSysImGroup == nil {
panic("implement not found for interface ISysImGroup, forgot register?")
}
return localSysImGroup
}
func RegisterSysImGroup(i ISysImGroup) {
localSysImGroup = i
}
func SysImRealname() ISysImRealname {
if localSysImRealname == nil {
panic("implement not found for interface ISysImRealname, forgot register?")
}
return localSysImRealname
}
func RegisterSysImRealname(i ISysImRealname) {
localSysImRealname = i
}
func SysImWorkOrder() ISysImWorkOrder {
if localSysImWorkOrder == nil {
panic("implement not found for interface ISysImWorkOrder, forgot register?")
}
return localSysImWorkOrder
}
func RegisterSysImWorkOrder(i ISysImWorkOrder) {
localSysImWorkOrder = i
}
func SysImAuditLog() ISysImAuditLog {
if localSysImAuditLog == nil {
panic("implement not found for interface ISysImAuditLog, forgot register?")
}
return localSysImAuditLog
}
func RegisterSysImAuditLog(i ISysImAuditLog) {
localSysImAuditLog = i
}
func SysDictData() ISysDictData {
if localSysDictData == nil {
panic("implement not found for interface ISysDictData, forgot register?")
+1
View File
@@ -7,6 +7,7 @@ package main
import (
_ "hotgo/internal/packed"
_ "time/tzdata"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
+10 -1
View File
@@ -237,6 +237,9 @@ database:
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
debug: true
Prefix: "hg_"
imRegistry:
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
debug: true
# 链路追踪
@@ -300,4 +303,10 @@ hggen:
addon:
srcPath: "./resource/generate/default/addon" # 生成模板路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
imAdminApi:
baseUrl: "http://127.0.0.1:6060"
serviceId: "hotgo-admin"
serviceSecret: "${IM_ADMIN_API_SECRET}"
timeout: "5s"
+15
View File
@@ -0,0 +1,15 @@
FROM alpine:3.20
WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata
COPY temp/linux_arm64/hotgo /app/hotgo
COPY hack /app/hack
COPY manifest/config /app/manifest/config
COPY resource /app/resource
COPY storage /app/storage
EXPOSE 8000
CMD ["/app/hotgo"]
@@ -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 @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' 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, '审计日志', 'imAuditLog', 'audit-log', '', '1', '/im/audit-log/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '60', '', '1', @now, @now
WHERE @auditDirId IS NULL;
SELECT @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' LIMIT 1;
SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' 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, @auditDirId, '审计列表', 'imAuditLogIndex', 'index', '', '2', '', '/imAuditLog/list', '', '/imAuditLog/index', '1', 'imAuditLog', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' '), '10', '', '1', @now, @now
WHERE @auditListId IS NULL;
SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' 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, @auditListId, '审计详情', 'imAuditLogView', '', '', '3', '', '/imAuditLog/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' tr_', @auditListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAuditLogView');
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS im_audit_logs (
id BIGINT NOT NULL AUTO_INCREMENT,
actor_uid BIGINT NOT NULL DEFAULT 0,
actor_role VARCHAR(64) NOT NULL DEFAULT '',
action VARCHAR(128) NOT NULL,
target_type VARCHAR(64) NOT NULL DEFAULT '',
target_id VARCHAR(128) NOT NULL DEFAULT '',
before_snapshot JSON,
after_snapshot JSON,
reason VARCHAR(512) NOT NULL DEFAULT '',
request_id VARCHAR(64) NOT NULL DEFAULT '',
ip VARCHAR(64) NOT NULL DEFAULT '',
user_agent VARCHAR(512) NOT NULL DEFAULT '',
metadata JSON,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_im_audit_logs_actor (actor_uid, id DESC),
INDEX idx_im_audit_logs_action (action, id DESC),
INDEX idx_im_audit_logs_target (target_type, target_id, id DESC),
INDEX idx_im_audit_logs_request (request_id),
INDEX idx_im_audit_logs_created (created_at, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM租户审计日志';
@@ -0,0 +1,59 @@
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 @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' 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, '群组管理', 'imGroup', 'group', '', '1', '/im/group/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '30', '', '1', @now, @now
WHERE @groupDirId IS NULL;
SELECT @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' LIMIT 1;
SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' 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, @groupDirId, '群组列表', 'imGroupIndex', 'index', '', '2', '', '/imGroup/list', '', '/imGroup/index', '1', 'imGroup', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' '), '10', '', '1', @now, @now
WHERE @groupListId IS NULL;
SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' 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, @groupListId, '群组详情', 'imGroupView', '', '', '3', '', '/imGroup/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupView');
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, '群成员列表', 'imGroupMemberList', '', '', '3', '', '/imGroup/memberList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberList');
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, '群治理配置', '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, '入群申请列表', '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, '群邀请列表', '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');
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, '群公告列表', 'imGroupAnnouncementList', '', '', '3', '', '/imGroup/announcementList', '', '', '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` = 'imGroupAnnouncementList');
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, '群禁言列表', 'imGroupMuteList', '', '', '3', '', '/imGroup/muteList', '', '', '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` = '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');
@@ -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 @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' 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, '实名审核', 'imRealname', 'realname', '', '1', '/im/realname/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '40', '', '1', @now, @now
WHERE @realnameDirId IS NULL;
SELECT @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' LIMIT 1;
SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' 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, @realnameDirId, '审核列表', 'imRealnameIndex', 'index', '', '2', '', '/imRealname/list', '', '/imRealname/index', '1', 'imRealname', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' '), '10', '', '1', @now, @now
WHERE @realnameListId IS NULL;
SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' 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, @realnameListId, '实名详情', 'imRealnameView', '', '', '3', '', '/imRealname/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameView');
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, @realnameListId, '实名审核操作', 'imRealnameReview', '', '', '3', '', '/imRealname/review', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameReview');
@@ -0,0 +1,43 @@
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 @invitePageId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRegistrationInviteIndex' 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, '注册邀请码', 'imRegistrationInviteIndex', 'registrationInvite', '', '2', '', '/imInvite/list', '', '/im/registrationInvite/index', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '70', '', '1', @now, @now
WHERE @invitePageId IS NULL;
SELECT @invitePageId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRegistrationInviteIndex' 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, @invitePageId, '生成邀请码', 'imInviteCreate', '', '', '3', '', '/imInvite/create', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteCreate');
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, @invitePageId, '批量生成邀请码', 'imInviteBatchCreate', '', '', '3', '', '/imInvite/batchCreate', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchCreate');
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, @invitePageId, '批次详情', 'imInviteBatchView', '', '', '3', '', '/imInvite/batchView', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchView');
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, @invitePageId, '撤销邀请码', 'imInviteRevoke', '', '', '3', '', '/imInvite/revoke', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteRevoke');
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, @invitePageId, '批量撤销邀请码', 'imInviteBatchRevoke', '', '', '3', '', '/imInvite/batchRevoke', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteBatchRevoke');
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, @invitePageId, '导出邀请码', 'imInviteExport', '', '', '3', '', '/imInvite/export', '', '', '1', '', '0', '0', '', '0', '1', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @invitePageId, ' '), '60', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imInviteExport');
@@ -0,0 +1,39 @@
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 @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' 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, '租户管理', 'imTenant', 'tenant', '', '1', '/im/tenant/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '10', '', '1', @now, @now
WHERE @tenantDirId IS NULL;
SELECT @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' LIMIT 1;
SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' 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, @tenantDirId, '租户列表', 'imTenantIndex', 'index', '', '2', '', '/imTenant/list', '', '/imTenant/index', '1', 'imTenant', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' '), '10', '', '1', @now, @now
WHERE @tenantListId IS NULL;
SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' 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, @tenantListId, '租户详情', 'imTenantView', '', '', '3', '', '/imTenant/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantView');
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, @tenantListId, '编辑/新增租户', 'imTenantEdit', '', '', '3', '', '/imTenant/edit', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantEdit');
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, @tenantListId, '修改租户状态', 'imTenantStatus', '', '', '3', '', '/imTenant/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantStatus');
@@ -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 @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' 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, '用户管理', 'imUser', 'user', '', '1', '/im/user/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '20', '', '1', @now, @now
WHERE @userDirId IS NULL;
SELECT @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' LIMIT 1;
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' 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, @userDirId, '用户列表', 'imUserIndex', 'index', '', '2', '', '/imUser/list', '', '/imUser/index', '1', 'imUser', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' '), '10', '', '1', @now, @now
WHERE @userListId IS NULL;
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' 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, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUser/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView');
@@ -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 @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' 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, '举报反馈', 'imWorkOrder', 'work-order', '', '1', '/im/work-order/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '50', '', '1', @now, @now
WHERE @workOrderDirId IS NULL;
SELECT @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' LIMIT 1;
SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' 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, @workOrderDirId, '工单列表', 'imWorkOrderIndex', 'index', '', '2', '', '/imWorkOrder/list', '', '/imWorkOrder/index', '1', 'imWorkOrder', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' '), '10', '', '1', @now, @now
WHERE @workOrderListId IS NULL;
SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' 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, @workOrderListId, '工单详情', 'imWorkOrderView', '', '', '3', '', '/imWorkOrder/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderView');
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, @workOrderListId, '更新工单状态', 'imWorkOrderStatus', '', '', '3', '', '/imWorkOrder/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderStatus');
+1
View File
@@ -223,6 +223,7 @@ COMMENT ON COLUMN hg_admin_dept.updated_at IS '更新时间';
CREATE TABLE IF NOT EXISTS hg_admin_member (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT,
dept_id BIGINT DEFAULT 0,
role_id BIGINT DEFAULT 10,
real_name VARCHAR(32) DEFAULT '',
+2
View File
@@ -213,6 +213,7 @@ INSERT INTO `hg_admin_dept` (`id`, `pid`, `name`, `code`, `type`, `leader`, `pho
CREATE TABLE IF NOT EXISTS `hg_admin_member` (
`id` bigint(20) NOT NULL COMMENT '管理员ID',
`tenant_id` bigint(20) DEFAULT NULL COMMENT 'IM租户ID',
`dept_id` bigint(20) DEFAULT '0' COMMENT '部门ID',
`role_id` bigint(20) DEFAULT '10' COMMENT '角色ID',
`real_name` varchar(32) DEFAULT '' COMMENT '真实姓名',
@@ -5789,6 +5790,7 @@ ALTER TABLE `hg_admin_dept`
ALTER TABLE `hg_admin_member`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `invite_code` (`invite_code`),
ADD KEY `idx_hg_admin_member_tenant` (`tenant_id`),
ADD KEY `dept_id` (`dept_id`),
ADD KEY `pid` (`pid`);
+61
View File
@@ -0,0 +1,61 @@
-- Repair IM admin menu titles if they were imported with a wrong client charset.
-- Safe to run repeatedly on HotGo's default hg_ prefixed backend database.
SET NAMES utf8mb4;
UPDATE hg_admin_menu
SET title = CASE name
WHEN 'imManager' THEN 'IM管理'
WHEN 'imTenant' THEN '租户管理'
WHEN 'imTenantIndex' THEN '租户列表'
WHEN 'imTenantView' THEN '租户详情'
WHEN 'imTenantEdit' THEN '编辑/新增租户'
WHEN 'imTenantStatus' THEN '修改租户状态'
WHEN 'imUser' THEN '用户管理'
WHEN 'imUserIndex' THEN '用户列表'
WHEN 'imUserView' THEN '用户详情'
WHEN 'imUserResetPassword' THEN '重置用户密码'
WHEN 'imGroup' THEN '群组管理'
WHEN 'imGroupIndex' THEN '群组列表'
WHEN 'imGroupView' THEN '群组详情'
WHEN 'imGroupMemberList' THEN '群成员列表'
WHEN 'imGroupGovernance' THEN '群治理配置'
WHEN 'imGroupJoinRequestList' THEN '入群申请列表'
WHEN 'imGroupInvitationList' THEN '群邀请列表'
WHEN 'imGroupAnnouncementList' THEN '群公告列表'
WHEN 'imGroupMuteList' THEN '群禁言列表'
WHEN 'imGroupAdminLogList' THEN '群管理日志'
WHEN 'imRealname' THEN '实名审核'
WHEN 'imRealnameIndex' THEN '审核列表'
WHEN 'imRealnameView' THEN '实名详情'
WHEN 'imRealnameReview' THEN '实名审核操作'
WHEN 'imWorkOrder' THEN '举报反馈'
WHEN 'imWorkOrderIndex' THEN '工单列表'
WHEN 'imWorkOrderView' THEN '工单详情'
WHEN 'imWorkOrderStatus' THEN '更新工单状态'
WHEN 'imAuditLog' THEN '审计日志'
WHEN 'imAuditLogIndex' THEN '审计列表'
WHEN 'imAuditLogView' THEN '审计详情'
WHEN 'imRegistrationInviteIndex' THEN '注册邀请码'
WHEN 'imInviteCreate' THEN '生成邀请码'
WHEN 'imInviteBatchCreate' THEN '批量生成邀请码'
WHEN 'imInviteBatchView' THEN '批次详情'
WHEN 'imInviteRevoke' THEN '撤销邀请码'
WHEN 'imInviteBatchRevoke' THEN '批量撤销邀请码'
WHEN 'imInviteExport' THEN '导出邀请码'
ELSE title
END
WHERE name IN (
'imManager',
'imTenant', 'imTenantIndex', 'imTenantView', 'imTenantEdit', 'imTenantStatus',
'imUser', 'imUserIndex', 'imUserView', 'imUserResetPassword',
'imGroup', 'imGroupIndex', 'imGroupView', 'imGroupMemberList', 'imGroupGovernance',
'imGroupJoinRequestList', 'imGroupInvitationList', 'imGroupAnnouncementList',
'imGroupMuteList', 'imGroupAdminLogList',
'imRealname', 'imRealnameIndex', 'imRealnameView', 'imRealnameReview',
'imWorkOrder', 'imWorkOrderIndex', 'imWorkOrderView', 'imWorkOrderStatus',
'imAuditLog', 'imAuditLogIndex', 'imAuditLogView',
'imRegistrationInviteIndex',
'imInviteCreate', 'imInviteBatchCreate', 'imInviteBatchView', 'imInviteRevoke',
'imInviteBatchRevoke', 'imInviteExport'
);
@@ -0,0 +1,118 @@
-- IM registration SMS switch and registration invite admin migration.
-- This file is idempotent for MySQL deployments using HotGo's default hg_ prefix.
SET NAMES utf8mb4;
-- im_tenant is the only tenant registry. Admin identities reference it directly.
SET @tenant_column_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND COLUMN_NAME = 'tenant_id'
);
SET @ddl := IF(
@tenant_column_exists = 0,
'ALTER TABLE hg_admin_member ADD COLUMN tenant_id BIGINT NULL COMMENT ''IM tenant ID'' AFTER id',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Preserve an existing deployment's old mapping before removing the redundant table.
SET @binding_table_exists := (
SELECT COUNT(*) FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_admin_tenant_binding'
);
SET @dml := IF(
@binding_table_exists > 0,
'UPDATE hg_admin_member AS m JOIN im_admin_tenant_binding AS b ON b.hotgo_tenant_id = m.id SET m.tenant_id = b.im_tenant_id WHERE b.status = 1 AND m.tenant_id IS NULL',
'SELECT 1'
);
PREPARE stmt FROM @dml;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DROP TABLE IF EXISTS im_admin_tenant_binding;
SET @tenant_index_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND INDEX_NAME = 'idx_hg_admin_member_tenant'
);
SET @ddl := IF(
@tenant_index_exists = 0,
'ALTER TABLE hg_admin_member ADD INDEX idx_hg_admin_member_tenant (tenant_id)',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @tenant_fk_exists := (
SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND CONSTRAINT_NAME = 'fk_hg_admin_member_im_tenant'
);
SET @ddl := IF(
@tenant_fk_exists = 0,
'ALTER TABLE hg_admin_member ADD CONSTRAINT fk_hg_admin_member_im_tenant FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS im_admin_operation_audit (
id BIGINT NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
tenant_id BIGINT NULL,
operator_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_id BIGINT NULL,
result VARCHAR(16) NOT NULL,
ip VARCHAR(64) NOT NULL DEFAULT '',
detail_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
KEY idx_im_admin_audit_tenant_time (tenant_id, created_at),
KEY idx_im_admin_audit_operator_time (operator_id, created_at),
UNIQUE KEY uk_im_admin_audit_request_action (request_id, action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT
'im', '注册短信发送', 'bool', 'register_sms_enabled', '1', '1', 10,
'控制 IM 注册验证码是否真实发送', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM hg_sys_config WHERE `group` = 'im' AND `key` = 'register_sms_enabled'
);
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, redirect, permissions, component, always_show, is_root, is_frame, keep_alive, hidden, affix, sort, status, created_at, updated_at)
SELECT
0, 1, '', 'IM管理', 'imManager', '/im', 'AppstoreOutlined', 1, '', '', 'LAYOUT', 0, 0, 1, 0, 0, 0, 125, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imManager');
SET @imRoot := (SELECT id FROM hg_admin_menu WHERE name = 'imManager' LIMIT 1);
UPDATE hg_admin_menu SET tree = CONCAT('tr_', id, ' '), component = 'LAYOUT' WHERE id = @imRoot;
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, redirect, permissions, component, always_show, is_root, is_frame, keep_alive, hidden, affix, sort, status, created_at, updated_at)
SELECT
@imRoot, 2, '', '注册邀请码', 'imRegistrationInviteIndex', 'registrationInvite', '', 2, '', '/imInvite/list', '/im/registrationInvite/index', 0, 0, 1, 0, 0, 0, 70, 1, NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imRegistrationInviteIndex');
SET @invitePage := (SELECT id FROM hg_admin_menu WHERE name = 'imRegistrationInviteIndex' LIMIT 1);
UPDATE hg_admin_menu SET tree = CONCAT('tr_', id, ' tr_', @imRoot, ' ') WHERE id = @invitePage;
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, sort, status, created_at, updated_at)
SELECT @invitePage, 3, CONCAT('tr_', @invitePage, ' tr_', @imRoot, ' '), title, name, '', '', 3, permissions, '', sort, 1, NOW(), NOW()
FROM (
SELECT '生成邀请码' title, 'imInviteCreate' name, '/imInvite/create' permissions, 10 sort UNION ALL
SELECT '批量生成邀请码', 'imInviteBatchCreate', '/imInvite/batchCreate', 20 UNION ALL
SELECT '批次详情', 'imInviteBatchView', '/imInvite/batchView', 30 UNION ALL
SELECT '撤销邀请码', 'imInviteRevoke', '/imInvite/revoke', 40 UNION ALL
SELECT '批量撤销邀请码', 'imInviteBatchRevoke', '/imInvite/batchRevoke', 50 UNION ALL
SELECT '导出邀请码', 'imInviteExport', '/imInvite/export', 60
) p
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
+2 -1
View File
@@ -100,6 +100,7 @@ CREATE TABLE `hg_admin_dept` ( -- 管理员_部门
);
CREATE TABLE `hg_admin_member` ( -- 管理员_用户表
`id` INTEGER NOT NULL PRIMARY KEY, -- 管理员ID
`tenant_id` INTEGER DEFAULT NULL, -- IM租户ID
`dept_id` INTEGER DEFAULT 0, -- 部门ID
`role_id` INTEGER DEFAULT 10, -- 角色ID
`real_name` TEXT DEFAULT '', -- 真实姓名
@@ -653,4 +654,4 @@ CREATE INDEX `hg_sys_provinces_pid` ON `hg_sys_provinces` (`pid`);
CREATE UNIQUE INDEX `hg_sys_serve_license_appid` ON `hg_sys_serve_license` (`appid`);
CREATE INDEX `hg_sys_serve_log_member_id` ON `hg_sys_serve_log` (`level_format`);
CREATE INDEX `hg_sys_serve_log_traceid` ON `hg_sys_serve_log` (`trace_id`);
CREATE INDEX `hg_sys_sms_log_mobile` ON `hg_sys_sms_log` (`mobile`);
CREATE INDEX `hg_sys_sms_log_mobile` ON `hg_sys_sms_log` (`mobile`);