feat: add tenant-aware IM management console

This commit is contained in:
richjohnrambo
2026-08-19 00:31:09 +08:00
parent 642b7ec5da
commit 89ba6bcff8
126 changed files with 11941 additions and 339 deletions
+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,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/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
}
func (c *cImUser) ResetPassword(ctx context.Context, req *imuser.ResetPasswordReq) (res *imuser.ResetPasswordRes, err error) {
err = service.SysImUser().ResetPassword(ctx, &req.ImUserPasswordResetInp)
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)
+147
View File
@@ -0,0 +1,147 @@
// Package imtenant provides helpers for accessing tenant business databases.
package imtenant
import (
"context"
"fmt"
"hash/fnv"
"hotgo/internal/dao"
"hotgo/internal/model/entity"
"net/url"
"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"
// 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().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
}
// RegistryDB returns the dedicated tenant registry connection, or the default
// HotGo database when a separate registry group is not configured.
func RegistryDB() gdb.DB {
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
return g.DB(registryDbGroup)
}
return g.DB()
}
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
if tenant.DbDsn != "" {
return normalizeMySQLTimeOptions(tenant.DbDsn)
}
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 normalizeMySQLTimeOptions(link[:dbStart] + dbName)
}
queryStart += dbStart
return normalizeMySQLTimeOptions(link[:dbStart] + dbName + link[queryStart:])
}
func normalizeMySQLTimeOptions(link string) (string, error) {
if !strings.HasPrefix(link, "mysql:") {
return "", gerror.New("租户业务库仅支持 MySQL")
}
parts := strings.SplitN(link, "?", 2)
values := make(url.Values)
if len(parts) == 2 {
var err error
values, err = url.ParseQuery(parts[1])
if err != nil {
return "", gerror.Wrap(err, "租户业务库连接参数格式不正确")
}
}
values.Set("loc", "UTC")
values.Set("parseTime", "true")
return parts[0] + "?" + values.Encode(), nil
}
func hash(s string) uint64 {
h := fnv.New64a()
_, _ = h.Write([]byte(s))
return h.Sum64()
}
@@ -0,0 +1,33 @@
package imtenant
import (
"context"
"strings"
"testing"
)
func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) {
link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "tinode")
if err != nil {
t.Fatal(err)
}
for _, expected := range []string{"/tinode?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} {
if !strings.Contains(link, expected) {
t.Fatalf("link %q does not contain %q", link, expected)
}
}
}
func TestNextSnowflakeID(t *testing.T) {
first, err := NextSnowflakeID(context.Background())
if err != nil {
t.Fatal(err)
}
second, err := NextSnowflakeID(context.Background())
if err != nil {
t.Fatal(err)
}
if first <= 0 || second <= first {
t.Fatalf("Snowflake IDs are not increasing: %d, %d", first, second)
}
}
@@ -0,0 +1,50 @@
package imtenant
import (
"context"
"fmt"
"os"
"strconv"
"sync"
"github.com/gogf/gf/v2/frame/g"
sf "github.com/tinode/snowflake"
)
const defaultSnowflakeWorkerID = 1023
var (
snowflakeOnce sync.Once
snowflakeGen *sf.SnowFlake
snowflakeErr error
)
// NextSnowflakeID returns an unencrypted Snowflake ID suitable for IM BIGINT keys.
func NextSnowflakeID(ctx context.Context) (int64, error) {
snowflakeOnce.Do(func() {
workerID := g.Cfg().MustGet(ctx, "imDirectDb.snowflakeWorkerId", defaultSnowflakeWorkerID).Int()
if value := os.Getenv("IM_DIRECT_DB_SNOWFLAKE_WORKER_ID"); value != "" {
workerID, snowflakeErr = strconv.Atoi(value)
if snowflakeErr != nil {
snowflakeErr = fmt.Errorf("invalid IM_DIRECT_DB_SNOWFLAKE_WORKER_ID: %w", snowflakeErr)
return
}
}
if workerID < 0 || workerID > 1023 {
snowflakeErr = fmt.Errorf("IM Snowflake worker ID must be between 0 and 1023")
return
}
snowflakeGen, snowflakeErr = sf.NewSnowFlake(uint32(workerID))
})
if snowflakeErr != nil {
return 0, snowflakeErr
}
id, err := snowflakeGen.Next()
if err != nil {
return 0, err
}
if id == 0 || id > uint64(^uint64(0)>>1) {
return 0, fmt.Errorf("generated invalid IM Snowflake ID")
}
return int64(id), nil
}
+23
View File
@@ -467,6 +467,29 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
return
}
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
if in.TenantId > 0 {
count, queryErr := g.DB("imRegistry").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,
+108
View File
@@ -0,0 +1,108 @@
package sys
import (
"context"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"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"
)
type imAdminScope struct {
TenantId int64
OperatorId int64
RoleKey string
RequestId string
}
const (
imAdminOperationAuditTable = "im_admin_operation_audit"
)
func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imAdminScope, err error) {
return resolveImAdminScopeWithAll(ctx, selectedTenantId, false)
}
func resolveImAdminScopeWithAll(ctx context.Context, selectedTenantId int64, allowAll bool) (scope imAdminScope, 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 {
if allowAll {
return scope, nil
}
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().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
}
+572
View File
@@ -0,0 +1,572 @@
package sys
import (
"context"
cryptorand "crypto/rand"
"encoding/csv"
"errors"
"fmt"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strconv"
"strings"
"time"
mysqldriver "github.com/go-sql-driver/mysql"
"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/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
)
const inviteGenerateRetries = 10
var errInviteBatchNotFound = errors.New("invite batch not found")
type sSysImInvite struct{}
func NewSysImInvite() *sSysImInvite { return &sSysImInvite{} }
func init() { service.RegisterSysImInvite(NewSysImInvite()) }
type imInviteListRequest struct {
TenantID int64
InviteCode string
Status string
BatchID int64
CreatedBy int64
CreatedAtStart *time.Time
CreatedAtEnd *time.Time
ExpiresAtStart *time.Time
ExpiresAtEnd *time.Time
}
type imInviteCodeResponse struct {
ID int64 `json:"id"`
TenantID int64 `json:"tenant_id"`
TenantName string `json:"tenant_name"`
BatchID *int64 `json:"batch_id"`
Code string `json:"code"`
Status string `json:"status"`
ExpiresAt time.Time `json:"expires_at"`
UsageCount int `json:"usage_count"`
LastUsedAt *time.Time `json:"last_used_at"`
CreatedBy int64 `json:"created_by"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"created_at"`
}
type imInviteBatchResponse struct {
BatchID int64 `json:"batch_id"`
TenantID int64 `json:"tenant_id"`
Quantity int `json:"quantity"`
IdempotencyKey string `json:"idempotency_key"`
Remark string `json:"remark"`
ActiveCount int `json:"active_count"`
UsageCount int `json:"usage_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, db, err := inviteQueryDB(ctx, in.TenantId)
if err != nil {
return nil, 0, err
}
req, err := buildInviteListRequest(in)
if err != nil {
return nil, 0, err
}
if !validInviteStatus(req.Status) {
return nil, 0, gerror.New("邀请码状态不正确")
}
req.TenantID = scope.TenantId
items, total, err := queryInviteCodes(ctx, db, req, in.PerPage, (in.Page-1)*in.PerPage)
result := "success"
if err != nil {
result = "failed"
} else {
list = convertInviteCodes(items)
totalCount = total
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, result, g.Map{"hasInviteCode": in.InviteCode != ""})
return list, totalCount, err
}
func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
var item imInviteCodeResponse
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key)
if createErr != nil {
err = createErr
} else if len(codes) > 0 {
item = codes[0]
} else {
codes, _, err = queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: batch.BatchID}, 1, 0)
if err == nil && len(codes) > 0 {
item = codes[0]
} else if err == nil {
err = gerror.New("邀请码生成结果不存在")
}
}
} else {
item, err = createSingleInvite(ctx, db, scope.TenantId, scope.OperatorId, expiresAt, in.Remark)
}
result := "success"
if err != nil {
result = "failed"
} else {
res = convertInviteCode(item)
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", item.ID, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark})
return res, err
}
func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
key := strings.TrimSpace(in.IdempotencyKey)
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key)
result := "success"
if err != nil {
result = "failed"
} else {
res = convertInviteBatch(batch)
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
return res, err
}
func (s *sSysImInvite) BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) {
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
id, err := sysin.ParseOptionalInt64(in.BatchId)
if err != nil {
return nil, err
}
batch, err := getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id)
if err != nil {
return nil, err
}
codes, _, err := queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: id}, batch.Quantity, 0)
if err != nil {
return nil, err
}
return &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(batch), List: convertInviteCodes(codes)}, nil
}
func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) {
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
return err
}
id, err := sysin.ParseOptionalInt64(in.Id)
if err != nil {
return err
}
now := time.Now().UTC().Round(time.Millisecond)
result, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=?
WHERE tenant_id=? AND id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now)
if err == nil {
if affected, _ := result.RowsAffected(); affected == 0 {
err = gerror.New("有效邀请码不存在")
}
}
auditResult := "success"
if err != nil {
auditResult = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, auditResult, nil)
return err
}
func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) {
scope, db, err := inviteDB(ctx, in.TenantId)
if err != nil {
return nil, err
}
id, err := sysin.ParseOptionalInt64(in.BatchId)
if err != nil {
return nil, err
}
if _, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id); err == nil {
now := time.Now().UTC().Round(time.Millisecond)
_, err = db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=?
WHERE tenant_id=? AND batch_id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now)
}
var batch *imInviteBatchResponse
if err == nil {
batch, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id)
}
auditResult := "success"
if err != nil {
auditResult = "failed"
} else {
res = convertInviteBatch(batch)
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", id, auditResult, nil)
return res, err
}
func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) {
if err = in.Filter(ctx); err != nil {
return err
}
scope, db, err := inviteQueryDB(ctx, in.TenantId)
if err != nil {
return err
}
req, err := buildInviteListRequest(in)
if err != nil || !validInviteStatus(req.Status) {
return gerror.New("导出条件不正确")
}
req.TenantID = scope.TenantId
request := ghttp.RequestFromCtx(ctx)
if request == nil {
return gerror.New("请求上下文不存在")
}
request.Response.Header().Set("Content-Type", "text/csv; charset=utf-8")
request.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=registration-invite-codes-%s.csv", time.Now().Format("20060102-150405")))
_, _ = request.Response.Writer.Write([]byte{0xef, 0xbb, 0xbf})
writer := csv.NewWriter(request.Response.Writer)
_ = writer.Write([]string{"tenant_id", "tenant_name", "code", "status", "batch_id", "expires_at", "usage_count", "last_used_at", "created_by", "created_at", "remark"})
for offset := 0; ; offset += 100 {
items, total, queryErr := queryInviteCodes(ctx, db, req, 100, offset)
if queryErr != nil {
return gerror.Wrap(queryErr, "导出邀请码失败")
}
for _, item := range items {
_ = writer.Write([]string{strconv.FormatInt(item.TenantID, 10), item.TenantName, item.Code, item.Status, formatOptionalInt64(item.BatchID), item.ExpiresAt.Format(time.RFC3339), strconv.Itoa(item.UsageCount), formatTime(item.LastUsedAt), strconv.FormatInt(item.CreatedBy, 10), item.CreatedAt.Format(time.RFC3339), item.Remark})
}
writer.Flush()
if writer.Error() != nil {
return writer.Error()
}
if offset+len(items) >= total || len(items) == 0 {
break
}
}
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""})
return nil
}
func inviteDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) {
scope, err := resolveImAdminScope(ctx, selectedTenantID)
if err != nil {
return scope, nil, err
}
return scope, imtenant.RegistryDB(), nil
}
func inviteQueryDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) {
scope, err := resolveImAdminScopeWithAll(ctx, selectedTenantID, true)
if err != nil {
return scope, nil, err
}
return scope, imtenant.RegistryDB(), nil
}
func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int64, expiresAt time.Time, remark string) (imInviteCodeResponse, error) {
for attempt := 0; attempt < inviteGenerateRetries; attempt++ {
id, err := imtenant.NextSnowflakeID(ctx)
if err != nil {
return imInviteCodeResponse{}, err
}
code, err := randomInviteCode()
if err != nil {
return imInviteCodeResponse{}, err
}
now := time.Now().UTC().Round(time.Millisecond)
_, err = db.Exec(ctx, `INSERT INTO im_registration_invite_codes
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,NULL,?,'active',?,0,NULL,?,?,?,?)`, id, tenantID, code, expiresAt, operatorID, remark, now, now)
if err == nil {
return imInviteCodeResponse{ID: id, TenantID: tenantID, Code: code, Status: "active", ExpiresAt: expiresAt, CreatedBy: operatorID, Remark: remark, CreatedAt: now}, nil
}
if !isDuplicateError(err) {
return imInviteCodeResponse{}, err
}
}
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
}
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
}
return existing, nil, nil
} else if !errors.Is(err, errInviteBatchNotFound) {
return nil, nil, err
}
for attempt := 0; attempt < inviteGenerateRetries; attempt++ {
batchID, err := imtenant.NextSnowflakeID(ctx)
if err != nil {
return nil, nil, err
}
codes := make([]imInviteCodeResponse, quantity)
seen := make(map[string]struct{}, quantity)
for i := range codes {
for {
code, randomErr := randomInviteCode()
if randomErr != nil {
return nil, nil, randomErr
}
if _, ok := seen[code]; ok {
continue
}
seen[code] = struct{}{}
codes[i].Code = code
break
}
codes[i].ID, err = imtenant.NextSnowflakeID(ctx)
if err != nil {
return nil, nil, err
}
codes[i].BatchID = &batchID
}
now := time.Now().UTC().Round(time.Millisecond)
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_batches
(id,tenant_id,quantity,idempotency_key,expires_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,?)`, batchID, tenantID, quantity, key, expiresAt, operatorID, remark, now, now); txErr != nil {
return txErr
}
for i := range codes {
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
VALUES(?,?,?,?,'active',?,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
return txErr
}
codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now
}
return nil
})
if err == nil {
return &imInviteBatchResponse{BatchID: batchID, TenantID: tenantID, Quantity: quantity, IdempotencyKey: key, Remark: remark, ActiveCount: quantity, ExpiresAt: expiresAt, CreatedAt: now}, codes, nil
}
if !isDuplicateError(err) {
return nil, nil, err
}
if existing, lookupErr := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); lookupErr == nil {
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
}
return existing, nil, nil
} else if !errors.Is(lookupErr, errInviteBatchNotFound) {
return nil, nil, lookupErr
}
}
return nil, nil, gerror.New("无法生成不重复的邀请码,请重试")
}
func queryInviteCodes(ctx context.Context, db gdb.DB, req imInviteListRequest, limit, offset int) ([]imInviteCodeResponse, int, error) {
now := time.Now().UTC().Round(time.Millisecond)
expireWhere := "status='active' AND expires_at<=?"
expireArgs := []any{now, now}
if req.TenantID > 0 {
expireWhere = "tenant_id=? AND " + expireWhere
expireArgs = []any{now, req.TenantID, now}
}
if _, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='expired',updated_at=? WHERE `+expireWhere, expireArgs...); err != nil {
return nil, 0, err
}
where, args := inviteWhere(req)
totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_registration_invite_codes c WHERE "+where, args...)
if err != nil {
return nil, 0, err
}
queryArgs := append(append([]any{}, args...), limit, offset)
items := make([]imInviteCodeResponse, 0)
err = db.Raw(`SELECT c.id,c.tenant_id,t.name tenant_name,c.batch_id,c.code,c.status,c.expires_at,c.created_by,c.remark,c.created_at,
c.usage_count,c.last_used_at
FROM im_registration_invite_codes c
LEFT JOIN im_tenant t ON t.id=c.tenant_id
WHERE `+where+` ORDER BY c.created_at DESC,c.id DESC LIMIT ? OFFSET ?`, queryArgs...).Ctx(ctx).Scan(&items)
return items, totalValue.Int(), err
}
func inviteWhere(req imInviteListRequest) (string, []any) {
where := []string{"1=1"}
args := make([]any, 0, 8)
if req.TenantID > 0 {
where, args = append(where, "c.tenant_id=?"), append(args, req.TenantID)
}
if req.InviteCode != "" {
where, args = append(where, "c.code=?"), append(args, req.InviteCode)
}
if req.Status != "" {
where, args = append(where, "c.status=?"), append(args, req.Status)
}
if req.BatchID > 0 {
where, args = append(where, "c.batch_id=?"), append(args, req.BatchID)
}
if req.CreatedBy > 0 {
where, args = append(where, "c.created_by=?"), append(args, req.CreatedBy)
}
if req.CreatedAtStart != nil {
where, args = append(where, "c.created_at>=?"), append(args, *req.CreatedAtStart)
}
if req.CreatedAtEnd != nil {
where, args = append(where, "c.created_at<=?"), append(args, *req.CreatedAtEnd)
}
if req.ExpiresAtStart != nil {
where, args = append(where, "c.expires_at>=?"), append(args, *req.ExpiresAtStart)
}
if req.ExpiresAtEnd != nil {
where, args = append(where, "c.expires_at<=?"), append(args, *req.ExpiresAtEnd)
}
return strings.Join(where, " AND "), args
}
func getInviteBatch(ctx context.Context, db gdb.DB, tenantID int64, where string, arg any) (*imInviteBatchResponse, error) {
items := make([]imInviteBatchResponse, 0, 1)
now := time.Now().UTC().Round(time.Millisecond)
err := db.Raw(`SELECT b.id batch_id,b.tenant_id,b.quantity,b.idempotency_key,b.expires_at,b.remark,b.created_at,
COALESCE(SUM(CASE WHEN c.status='active' AND c.expires_at>? THEN 1 ELSE 0 END),0) active_count,
COALESCE(SUM(c.usage_count),0) usage_count,
COALESCE(SUM(CASE WHEN (c.status='active' AND c.expires_at<=?) OR c.status='expired' THEN 1 ELSE 0 END),0) expired_count,
COALESCE(SUM(CASE WHEN c.status='revoked' THEN 1 ELSE 0 END),0) revoked_count
FROM im_registration_invite_batches b LEFT JOIN im_registration_invite_codes c ON c.tenant_id=b.tenant_id AND c.batch_id=b.id
WHERE b.tenant_id=? AND `+where+` GROUP BY b.id LIMIT 1`, now, now, tenantID, arg).Ctx(ctx).Scan(&items)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, gerror.Wrap(errInviteBatchNotFound, "邀请码批次不存在")
}
return &items[0], nil
}
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{InviteCode: in.InviteCode, Status: in.Status, BatchID: batchID, CreatedBy: createdBy,
CreatedAtStart: nativeTime(in.CreatedAtStart), CreatedAtEnd: nativeTime(in.CreatedAtEnd),
ExpiresAtStart: nativeTime(in.ExpiresAtStart), ExpiresAtEnd: nativeTime(in.ExpiresAtEnd)}, nil
}
func randomInviteCode() (string, error) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
result := make([]byte, 5)
buffer := make([]byte, 10)
for index := 0; index < len(result); {
if _, err := cryptorand.Read(buffer); err != nil {
return "", err
}
for _, value := range buffer {
if value >= 208 {
continue
}
result[index] = alphabet[int(value)%len(alphabet)]
index++
if index == len(result) {
break
}
}
}
return string(result), nil
}
func isDuplicateError(err error) bool {
var mysqlErr *mysqldriver.MySQLError
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062
}
func validInviteStatus(status string) bool {
return status == "" || status == "active" || status == "expired" || status == "revoked"
}
func sameInviteBatch(batch *imInviteBatchResponse, quantity int, expiresAt time.Time, remark string) bool {
return batch != nil && batch.Quantity == quantity &&
batch.ExpiresAt.UTC().Round(time.Millisecond).Equal(expiresAt.UTC().Round(time.Millisecond)) && batch.Remark == remark
}
func convertInviteCodes(items []imInviteCodeResponse) []*sysin.ImInviteCodeModel {
list := make([]*sysin.ImInviteCodeModel, 0, len(items))
for _, item := range items {
list = append(list, convertInviteCode(item))
}
return list
}
func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel {
return &sysin.ImInviteCodeModel{Id: strconv.FormatInt(item.ID, 10), BatchId: formatOptionalInt64(item.BatchID), Code: item.Code,
TenantId: strconv.FormatInt(item.TenantID, 10), TenantName: item.TenantName,
Status: item.Status, ExpiresAt: gtime.New(item.ExpiresAt), UsageCount: item.UsageCount, LastUsedAt: convertTime(item.LastUsedAt),
CreatedBy: strconv.FormatInt(item.CreatedBy, 10), Remark: item.Remark, CreatedAt: gtime.New(item.CreatedAt)}
}
func convertInviteBatch(item *imInviteBatchResponse) *sysin.ImInviteBatchModel {
if item == nil {
return nil
}
return &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(item.BatchID, 10), Quantity: item.Quantity, ActiveCount: item.ActiveCount,
TenantId: strconv.FormatInt(item.TenantID, 10),
UsageCount: item.UsageCount, RevokedCount: item.RevokedCount, ExpiredCount: item.ExpiredCount,
ExpiresAt: gtime.New(item.ExpiresAt), CreatedAt: gtime.New(item.CreatedAt)}
}
func nativeTime(value *gtime.Time) *time.Time {
if value == nil {
return nil
}
t := value.Time.UTC()
return &t
}
func convertTime(value *time.Time) *gtime.Time {
if value == nil {
return nil
}
return gtime.New(value)
}
func formatOptionalInt64(value *int64) string {
if value == nil || *value == 0 {
return ""
}
return strconv.FormatInt(*value, 10)
}
func formatTime(value *time.Time) string {
if value == nil {
return ""
}
return value.Format(time.RFC3339)
}
func batchID(batch *imInviteBatchResponse) int64 {
if batch == nil {
return 0
}
return batch.BatchID
}
+124
View File
@@ -0,0 +1,124 @@
package sys
import (
"context"
"os"
"regexp"
"testing"
"time"
"hotgo/internal/library/imtenant"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/database/gdb"
"golang.org/x/crypto/bcrypt"
)
func TestRandomInviteCode(t *testing.T) {
pattern := regexp.MustCompile(`^[A-Za-z]{5}$`)
for i := 0; i < 100; i++ {
code, err := randomInviteCode()
if err != nil {
t.Fatal(err)
}
if !pattern.MatchString(code) {
t.Fatalf("invalid invite code %q", code)
}
}
}
func TestInviteDirectDatabase(t *testing.T) {
link := os.Getenv("IM_MANAGER_INTEGRATION_DSN")
if link == "" {
t.Skip("IM_MANAGER_INTEGRATION_DSN is not set")
}
const group = "im_invite_integration"
if err := gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
t.Fatal(err)
}
db, err := gdb.Instance(group)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
key := "integration-" + time.Now().UTC().Format("20060102150405.000000000")
expiresAt := time.Now().UTC().Add(time.Hour).Round(time.Millisecond)
batch, codes, err := createInviteBatch(ctx, db, 1, 1, 3, expiresAt, "integration", key)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = db.Exec(ctx, "DELETE FROM im_registration_invite_codes WHERE tenant_id=? AND batch_id=?", 1, batch.BatchID)
_, _ = db.Exec(ctx, "DELETE FROM im_registration_invite_batches WHERE tenant_id=? AND id=?", 1, batch.BatchID)
})
if batch.ActiveCount != 3 || len(codes) != 3 {
t.Fatalf("unexpected batch result: %#v codes=%d", batch, len(codes))
}
replayed, replayCodes, err := createInviteBatch(ctx, db, 1, 1, 3, expiresAt, "integration", key)
if err != nil {
t.Fatal(err)
}
if replayed.BatchID != batch.BatchID || len(replayCodes) != 0 {
t.Fatalf("idempotent replay created a new batch: %#v", replayed)
}
items, total, err := queryInviteCodes(ctx, db, imInviteListRequest{TenantID: 1, BatchID: batch.BatchID}, 10, 0)
if err != nil {
t.Fatal(err)
}
if total != 3 || len(items) != 3 {
t.Fatalf("unexpected query result total=%d items=%d", total, len(items))
}
}
func TestResetPasswordDirectDatabase(t *testing.T) {
link := os.Getenv("IM_MANAGER_INTEGRATION_DSN")
if link == "" {
t.Skip("IM_MANAGER_INTEGRATION_DSN is not set")
}
const group = "im_password_reset_integration"
if err := gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
t.Fatal(err)
}
db, err := gdb.Instance(group)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
userID, err := imtenant.NextSnowflakeID(ctx)
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC().Round(time.Millisecond)
username := "integration" + time.Now().UTC().Format("150405000000")
if _, err = db.Exec(ctx, `INSERT INTO im_users(id,username,createdat,updatedat,state,useragent)
VALUES(?,?,?,?,0,'integration-test')`, userID, username, now, now); err != nil {
t.Fatal(err)
}
if _, err = db.Exec(ctx, "INSERT INTO im_auth(userid,authlvl,secret) VALUES(?,20,'old')", userID); err != nil {
_, _ = db.Exec(ctx, "DELETE FROM im_users WHERE id=?", userID)
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = db.Exec(ctx, "DELETE FROM im_business_events_outbox WHERE target=? AND event_type='security.password_reset'", userID)
_, _ = db.Exec(ctx, "DELETE FROM im_user_security WHERE user_id=?", userID)
_, _ = db.Exec(ctx, "DELETE FROM im_auth WHERE userid=?", userID)
_, _ = db.Exec(ctx, "DELETE FROM im_users WHERE id=?", userID)
})
scope := imAdminScope{TenantId: 1, OperatorId: 99, RequestId: "integration-password-reset"}
if err = resetIMUserPassword(ctx, db, scope, userID, "new-password"); err != nil {
t.Fatal(err)
}
secret, err := db.GetValue(ctx, "SELECT secret FROM im_auth WHERE userid=?", userID)
if err != nil || bcrypt.CompareHashAndPassword(secret.Bytes(), []byte("new-password")) != nil {
t.Fatalf("password hash was not updated: %v", err)
}
version, err := db.GetValue(ctx, "SELECT auth_version FROM im_user_security WHERE user_id=?", userID)
if err != nil || version.Int() != 2 {
t.Fatalf("unexpected auth version %d: %v", version.Int(), err)
}
events, err := db.GetValue(ctx, `SELECT COUNT(*) FROM im_business_events_outbox
WHERE target=? AND event_type='security.password_reset' AND status='pending'`, userID)
if err != nil || events.Int() != 1 {
t.Fatalf("password reset event not written: count=%d err=%v", events.Int(), err)
}
}
+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
}
+274
View File
@@ -0,0 +1,274 @@
// 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/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{}
const imRegistryDbGroup = "imRegistry"
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(g.DB(imRegistryDbGroup).Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
}
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
return g.DB(imRegistryDbGroup).Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
}
// 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 g.DB(imRegistryDbGroup).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
}
+315
View File
@@ -0,0 +1,315 @@
// 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"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/guid"
"golang.org/x/crypto/bcrypt"
)
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
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, scope.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(
"CAST(u.id AS CHAR) user_id",
"u.username",
"COALESCE(u.phone, '') phone",
"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 userId, _ := sysin.ParseOptionalInt64(in.UserId); userId > 0 {
mod = mod.Where("u.id", userId)
}
if in.Nickname != "" {
mod = mod.WhereLike("up.nickname", "%"+in.Nickname+"%")
}
if in.AuthName != "" {
mod = mod.Where("u.username = ? OR u.phone = ? OR up.email = ?", in.AuthName, in.AuthName, 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 {
userId, parseErr := sysin.ParseOptionalInt64(item.UserId)
if parseErr != nil {
return parseErr
}
ids = append(ids, userId)
userMap[userId] = item
}
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 = item.Username
if item.Phone != "" && item.Phone != item.Username {
item.AuthNames += ", " + item.Phone
}
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
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
if err != nil {
return
}
userId, err := sysin.ParseOptionalInt64(in.UserId)
if err != nil {
return nil, err
}
fields := append(s.viewFields(), "u.access access_json", "u.public profile_json", "u.trusted trusted_json")
err = db.Model("im_users u").Safe().Ctx(ctx).
LeftJoin("im_user_profiles up", "up.user_id = u.id").
LeftJoin("im_user_security us", "us.user_id = u.id").
Fields(fields...).
Where("u.id", userId).
Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取IM用户信息失败,请稍后重试!")
}
if res == nil {
return nil, nil
}
res.TenantId = tenant.Id
res.TenantName = tenant.Name
list := []*sysin.ImUserListModel{&res.ImUserListModel}
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
return nil, err
}
return
}
func (s *sSysImUser) viewFields() []interface{} {
return []interface{}{
"CAST(u.id AS CHAR) user_id",
"u.username",
"COALESCE(u.phone, '') phone",
"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",
}
}
func (s *sSysImUser) ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return err
}
userId, err := sysin.ParseOptionalInt64(in.UserId)
if err != nil {
return err
}
db, _, err := imtenant.DB(ctx, scope.TenantId)
if err != nil {
return err
}
err = resetIMUserPassword(ctx, db, scope, userId, in.NewPassword)
result := "success"
if err != nil {
result = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "user_password_reset", "user", userId, result, nil)
return err
}
func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, newPassword string) error {
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return gerror.Wrap(err, "生成密码摘要失败")
}
securityId, err := imtenant.NextSnowflakeID(ctx)
if err != nil {
return gerror.Wrap(err, "生成安全记录ID失败")
}
eventId, err := imtenant.NextSnowflakeID(ctx)
if err != nil {
return gerror.Wrap(err, "生成安全事件ID失败")
}
now := time.Now().UTC().Round(time.Millisecond)
requestId := scope.RequestId
if requestId == "" {
requestId = gctx.CtxId(ctx)
}
if requestId == "" {
requestId = guid.S()
}
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("password-reset:%d:%d:%s", scope.TenantId, userId, requestId)))
eventKey := hex.EncodeToString(eventKeyBytes[:])
payload, _ := json.Marshal(map[string]any{
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId,
})
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId)
if txErr != nil {
return txErr
}
if !exists.Bool() {
return gerror.New("IM用户不存在")
}
result, txErr := tx.Exec("UPDATE im_auth SET secret=? WHERE userid=?", passwordHash, userId)
if txErr != nil {
return txErr
}
if affected, _ := result.RowsAffected(); affected == 0 {
return gerror.New("用户认证记录不存在")
}
if _, txErr = tx.Exec(`INSERT INTO im_user_security
(id,user_id,auth_version,password_updated_at,created_at,updated_at)
VALUES(?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,
password_updated_at=VALUES(password_updated_at),updated_at=VALUES(updated_at)`,
securityId, userId, 2, now, now, now); txErr != nil {
return txErr
}
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId)
if txErr != nil {
return txErr
}
var eventPayload map[string]any
_ = json.Unmarshal(payload, &eventPayload)
eventPayload["auth_version"] = version.Int()
payload, _ = json.Marshal(eventPayload)
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
(id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user",
fmt.Sprintf("%d", userId), 0, "security.password_reset", string(payload), now, now)
return txErr
})
}
+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,154 @@
package sysin
import (
"context"
"hotgo/internal/model/input/form"
"strconv"
"strings"
"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.Page <= 0 {
in.Page = 1
}
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) {
in.IdempotencyKey = strings.TrimSpace(in.IdempotencyKey)
if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) {
return gerror.New("到期时间必须晚于当前时间")
}
if len(in.Remark) > 255 {
return gerror.New("备注不能超过255个字符")
}
if len(in.IdempotencyKey) > 128 {
return gerror.New("幂等键不能超过128个字符")
}
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) {
in.IdempotencyKey = strings.TrimSpace(in.IdempotencyKey)
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个字符")
}
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
}
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"`
TenantId string `json:"tenantId" dc:"IM租户ID"`
TenantName string `json:"tenantName" dc:"IM租户名称"`
BatchId string `json:"batchId" dc:"批次ID"`
Code string `json:"code" dc:"邀请码"`
Status string `json:"status" dc:"状态"`
ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
UsageCount int `json:"usageCount" dc:"累计使用次数"`
LastUsedAt *gtime.Time `json:"lastUsedAt" dc:"最后使用时间"`
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"`
TenantId string `json:"tenantId" dc:"IM租户ID"`
Quantity int `json:"quantity" dc:"数量"`
ActiveCount int `json:"activeCount" dc:"有效数量"`
UsageCount int `json:"usageCount" 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,99 @@
// 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 string `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.UserId != "" {
if _, err = ParseOptionalInt64(in.UserId); err != nil {
return err
}
}
return
}
type ImUserListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
UserId string `json:"userId" dc:"用户ID"`
Username string `json:"username" dc:"登录账号"`
Phone string `json:"phone" dc:"手机号"`
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" dc:"租户ID"`
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
func (in *ImUserViewInp) Filter(ctx context.Context) (err error) {
if _, err = ParseOptionalInt64(in.UserId); err != nil {
return gerror.New("用户ID不能为空")
}
return
}
type ImUserPasswordResetInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
NewPassword string `json:"newPassword" v:"required#新密码不能为空" dc:"新密码"`
}
func (in *ImUserPasswordResetInp) Filter(ctx context.Context) error {
if _, err := ParseOptionalInt64(in.UserId); err != nil {
return err
}
if len([]rune(in.NewPassword)) < 3 {
return gerror.New("新密码长度不能少于3位")
}
if len([]byte(in.NewPassword)) > 72 {
return gerror.New("新密码长度不能超过72字节")
}
return nil
}
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资料"`
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,7 @@
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,7 @@
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举报反馈
}
+180
View File
@@ -171,6 +171,90 @@ 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)
// ResetPassword 管理员重置IM用户密码
ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (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 +498,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 +609,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?")