feat: expand IM admin controls

This commit is contained in:
richjohnrambo
2026-08-21 15:09:39 +08:00
parent c3676f9744
commit 543c7b624e
29 changed files with 1308 additions and 39 deletions
@@ -22,6 +22,14 @@ type ListRes struct {
List []*sysin.ImAuditLogListModel `json:"list" dc:"数据列表"`
}
// ExportReq 导出IM审计日志列表
type ExportReq struct {
g.Meta `path:"/imAuditLog/export" method:"get" tags:"IM审计日志" summary:"导出IM审计日志列表"`
sysin.ImAuditLogListInp
}
type ExportRes struct{}
// ViewReq 获取IM审计日志指定信息
type ViewReq struct {
g.Meta `path:"/imAuditLog/view" method:"get" tags:"IM审计日志" summary:"获取IM审计日志指定信息"`
+32
View File
@@ -48,3 +48,35 @@ type StatusReq struct {
}
type StatusRes struct{}
type UsageReq struct {
g.Meta `path:"/imTenant/usage" method:"get" tags:"IM租户" summary:"获取IM租户用量和健康状态"`
sysin.ImTenantUsageInp
}
type UsageRes struct {
*sysin.ImTenantUsageModel
}
type DeletionCheckReq struct {
g.Meta `path:"/imTenant/deletionCheck" method:"get" tags:"IM租户" summary:"IM租户删除前检查"`
sysin.ImTenantDeletionCheckInp
}
type DeletionCheckRes struct {
*sysin.ImTenantDeletionCheckModel
}
type EnterDeletingReq struct {
g.Meta `path:"/imTenant/enterDeleting" method:"post" tags:"IM租户" summary:"IM租户进入删除流程"`
sysin.ImTenantLifecycleInp
}
type EnterDeletingRes struct{}
type RestoreReq struct {
g.Meta `path:"/imTenant/restore" method:"post" tags:"IM租户" summary:"恢复IM租户"`
sysin.ImTenantLifecycleInp
}
type RestoreRes struct{}
+24
View File
@@ -39,3 +39,27 @@ type ResetPasswordReq struct {
}
type ResetPasswordRes struct{}
type RevokeSessionsReq struct {
g.Meta `path:"/imUser/revokeSessions" method:"post" tags:"IM用户" summary:"撤销IM用户会话"`
sysin.ImUserRevokeSessionsInp
}
type RevokeSessionsRes struct{}
type StatusReq struct {
g.Meta `path:"/imUser/status" method:"post" tags:"IM用户" summary:"更新IM用户状态"`
sysin.ImUserStatusInp
}
type StatusRes struct{}
type DevicesReq struct {
g.Meta `path:"/imUser/devices" method:"get" tags:"IM用户" summary:"获取IM用户设备列表"`
sysin.ImUserDeviceListInp
}
type DevicesRes struct {
form.PageRes
List []*sysin.ImUserDeviceListModel `json:"list" dc:"设备列表"`
}
@@ -31,6 +31,12 @@ func (c *cImAuditLog) List(ctx context.Context, req *imauditlog.ListReq) (res *i
return
}
// Export 导出IM审计日志列表
func (c *cImAuditLog) Export(ctx context.Context, req *imauditlog.ExportReq) (res *imauditlog.ExportRes, err error) {
err = service.SysImAuditLog().Export(ctx, &req.ImAuditLogListInp)
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)
@@ -58,3 +58,35 @@ func (c *cImTenant) Status(ctx context.Context, req *imtenant.StatusReq) (res *i
err = service.SysImTenant().Status(ctx, &req.ImTenantStatusInp)
return
}
func (c *cImTenant) Usage(ctx context.Context, req *imtenant.UsageReq) (res *imtenant.UsageRes, err error) {
data, err := service.SysImTenant().Usage(ctx, &req.ImTenantUsageInp)
if err != nil {
return
}
res = new(imtenant.UsageRes)
res.ImTenantUsageModel = data
return
}
func (c *cImTenant) DeletionCheck(ctx context.Context, req *imtenant.DeletionCheckReq) (res *imtenant.DeletionCheckRes, err error) {
data, err := service.SysImTenant().DeletionCheck(ctx, &req.ImTenantDeletionCheckInp)
if err != nil {
return
}
res = new(imtenant.DeletionCheckRes)
res.ImTenantDeletionCheckModel = data
return
}
func (c *cImTenant) EnterDeleting(ctx context.Context, req *imtenant.EnterDeletingReq) (res *imtenant.EnterDeletingRes, err error) {
err = service.SysImTenant().EnterDeleting(ctx, &req.ImTenantLifecycleInp)
return
}
func (c *cImTenant) Restore(ctx context.Context, req *imtenant.RestoreReq) (res *imtenant.RestoreRes, err error) {
err = service.SysImTenant().Restore(ctx, &req.ImTenantLifecycleInp)
return
}
@@ -51,3 +51,29 @@ func (c *cImUser) ResetPassword(ctx context.Context, req *imuser.ResetPasswordRe
err = service.SysImUser().ResetPassword(ctx, &req.ImUserPasswordResetInp)
return
}
func (c *cImUser) RevokeSessions(ctx context.Context, req *imuser.RevokeSessionsReq) (res *imuser.RevokeSessionsRes, err error) {
err = service.SysImUser().RevokeSessions(ctx, &req.ImUserRevokeSessionsInp)
return
}
func (c *cImUser) Status(ctx context.Context, req *imuser.StatusReq) (res *imuser.StatusRes, err error) {
err = service.SysImUser().Status(ctx, &req.ImUserStatusInp)
return
}
func (c *cImUser) Devices(ctx context.Context, req *imuser.DevicesReq) (res *imuser.DevicesRes, err error) {
list, totalCount, err := service.SysImUser().Devices(ctx, &req.ImUserDeviceListInp)
if err != nil {
return
}
if list == nil {
list = []*sysin.ImUserDeviceListModel{}
}
res = new(imuser.DevicesRes)
res.List = list
res.PageRes.Pack(req, totalCount)
return
}
+36 -2
View File
@@ -7,12 +7,18 @@ package sys
import (
"context"
"fmt"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/input/form"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
"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/gconv"
)
type sSysImAuditLog struct{}
@@ -30,7 +36,11 @@ func (s *sSysImAuditLog) List(ctx context.Context, in *sysin.ImAuditLogListInp)
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
if err != nil {
return
}
@@ -107,7 +117,11 @@ func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp)
if err = in.Filter(ctx); err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, in.TenantId)
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return nil, err
}
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
if err != nil {
return nil, err
}
@@ -123,3 +137,23 @@ func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp)
res.TenantName = tenant.Name
return
}
// Export 导出IM审计日志列表
func (s *sSysImAuditLog) Export(ctx context.Context, in *sysin.ImAuditLogListInp) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
tags, err := convert.GetEntityDescTags(sysin.ImAuditLogExportModel{})
if err != nil {
return
}
var exports []sysin.ImAuditLogExportModel
if err = gconv.Scan(list, &exports); err != nil {
return
}
fileName := "导出IM审计日志-" + gctx.CtxId(ctx)
sheetName := fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, form.CalPageCount(totalCount, in.PerPage), in.Page, len(list))
return excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
}
+137
View File
@@ -7,10 +7,13 @@ package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
@@ -272,3 +275,137 @@ func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp)
}
return
}
func (s *sSysImTenant) Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (res *sysin.ImTenantUsageModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
var tenant *entity.ImTenant
if err = s.Model(ctx).WherePri(in.Id).Scan(&tenant); err != nil {
return nil, gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
}
if tenant == nil || tenant.Id <= 0 {
return nil, gerror.New("IM租户不存在")
}
res = &sysin.ImTenantUsageModel{
Id: tenant.Id,
Code: tenant.Code,
Name: tenant.Name,
DbName: tenant.DbName,
DbDsnConfigured: tenant.DbDsn != "",
Warnings: []string{},
}
db, _, dbErr := imtenant.DB(ctx, tenant.Id)
if dbErr != nil {
res.DbReachable = false
res.DbCheckError = dbErr.Error()
return res, nil
}
if _, pingErr := db.GetValue(ctx, "SELECT 1"); pingErr != nil {
res.DbReachable = false
res.DbCheckError = pingErr.Error()
return res, nil
}
res.DbReachable = true
res.SchemaVersion = s.queryScalarString(ctx, db, "kvmeta.version", "SELECT value FROM kvmeta WHERE key_='version' LIMIT 1", &res.Warnings)
res.UserCount = s.queryScalarInt64(ctx, db, "im_users.total", "SELECT COUNT(*) FROM im_users", &res.Warnings)
res.ActiveUserCount = s.queryScalarInt64(ctx, db, "im_users.active", "SELECT COUNT(*) FROM im_users WHERE state=0", &res.Warnings)
res.GroupCount = s.queryScalarInt64(ctx, db, "im_group_settings", "SELECT COUNT(*) FROM im_group_settings WHERE group_status<>'dissolved'", &res.Warnings)
res.DeviceCount = s.queryScalarInt64(ctx, db, "im_devices", "SELECT COUNT(*) FROM im_devices", &res.Warnings)
res.PushTokenCount = s.queryScalarInt64(ctx, db, "im_device_push_tokens", "SELECT COUNT(*) FROM im_device_push_tokens", &res.Warnings)
res.InviteUseCount = s.queryScalarInt64(ctx, db, "im_registration_invite_uses", "SELECT COUNT(*) FROM im_registration_invite_uses", &res.Warnings)
res.OutboxPendingCount = s.queryScalarInt64(ctx, db, "im_business_events_outbox", "SELECT COUNT(*) FROM im_business_events_outbox WHERE status='pending'", &res.Warnings)
return
}
func (s *sSysImTenant) queryScalarInt64(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string) int64 {
value, err := db.GetValue(ctx, sql)
if err != nil {
*warnings = append(*warnings, label+": "+err.Error())
return 0
}
return value.Int64()
}
func (s *sSysImTenant) queryScalarString(ctx context.Context, db gdb.DB, label, sql string, warnings *[]string) string {
value, err := db.GetValue(ctx, sql)
if err != nil {
*warnings = append(*warnings, label+": "+err.Error())
return ""
}
return value.String()
}
func (s *sSysImTenant) DeletionCheck(ctx context.Context, in *sysin.ImTenantDeletionCheckInp) (res *sysin.ImTenantDeletionCheckModel, err error) {
if err = in.Filter(ctx); err != nil {
return
}
usage, err := s.Usage(ctx, &sysin.ImTenantUsageInp{Id: in.Id})
if err != nil {
return nil, err
}
res = &sysin.ImTenantDeletionCheckModel{
Id: usage.Id,
Code: usage.Code,
Name: usage.Name,
Blockers: []string{},
Warnings: usage.Warnings,
UserCount: usage.UserCount,
GroupCount: usage.GroupCount,
PendingOutbox: usage.OutboxPendingCount,
}
if !usage.DbReachable {
res.Blockers = append(res.Blockers, "租户业务库不可达:"+usage.DbCheckError)
}
if usage.UserCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 个用户", usage.UserCount))
}
if usage.GroupCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 个群", usage.GroupCount))
}
if usage.OutboxPendingCount > 0 {
res.Blockers = append(res.Blockers, fmt.Sprintf("租户仍有 %d 条待处理 Outbox", usage.OutboxPendingCount))
}
res.CanEnterDeleting = len(res.Blockers) == 0
return
}
func (s *sSysImTenant) EnterDeleting(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
check, err := s.DeletionCheck(ctx, &sysin.ImTenantDeletionCheckInp{Id: in.Id})
if err != nil {
return err
}
if !check.CanEnterDeleting {
return gerror.New("租户未通过删除前检查,不能进入删除流程")
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.ImTenant.Columns().State: consts.ImTenantStateDeleting,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
return gerror.Wrap(err, "设置IM租户删除流程失败,请稍后重试!")
}
return
}
func (s *sSysImTenant) Restore(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if _, err = s.Model(ctx).
WherePri(in.Id).
WhereIn(dao.ImTenant.Columns().State, []int{consts.ImTenantStateSuspended, consts.ImTenantStateDeleting}).
Data(g.Map{
dao.ImTenant.Columns().State: consts.ImTenantStateActive,
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
return gerror.Wrap(err, "恢复IM租户失败,请稍后重试!")
}
return
}
+208
View File
@@ -11,6 +11,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
@@ -248,6 +249,213 @@ func (s *sSysImUser) ResetPassword(ctx context.Context, in *sysin.ImUserPassword
return err
}
func (s *sSysImUser) RevokeSessions(ctx context.Context, in *sysin.ImUserRevokeSessionsInp) (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 = revokeIMUserSessions(ctx, db, scope, userId, "security.sessions_revoked")
result := "success"
if err != nil {
result = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "user_sessions_revoke", "user", userId, result, nil)
return err
}
func (s *sSysImUser) Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error) {
if err = in.Filter(ctx); err != nil {
return
}
if in.State != consts.ImUserStateOk && in.State != consts.ImUserStateSuspended {
return gerror.New("账号状态只能设置为正常或已停用")
}
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 = updateIMUserStatus(ctx, db, scope, userId, in.State)
result := "success"
if err != nil {
result = "failed"
}
_ = writeImAdminAudit(ctx, scope.TenantId, "user_status_update", "user", userId, result, map[string]any{"state": in.State})
return err
}
func (s *sSysImUser) Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, err error) {
if err = in.Filter(ctx); err != nil {
return
}
scope, err := resolveImAdminScope(ctx, in.TenantId)
if err != nil {
return
}
userId, err := sysin.ParseOptionalInt64(in.UserId)
if err != nil {
return
}
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
if err != nil {
return
}
mod := db.Model("im_devices d").Safe().Ctx(ctx).
LeftJoin("im_device_push_tokens p", "p.user_id=d.userid AND p.device_id=d.deviceid").
Fields(
"CAST(d.userid AS CHAR) user_id",
"d.deviceid device_id",
"COALESCE(d.platform, '') platform",
"COALESCE(d.lang, '') lang",
"d.lastseen last_seen",
"COALESCE(p.provider, '') push_provider",
"COALESCE(p.platform, '') push_platform",
"COALESCE(p.app_version, '') push_app_version",
"(p.id IS NOT NULL) push_token_configured",
"p.updated_at push_updated_at",
).
Where("d.userid", userId).
Page(in.Page, in.PerPage).
OrderDesc("d.lastseen")
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
return nil, 0, gerror.Wrap(err, "获取IM用户设备列表失败,请稍后重试!")
}
for _, item := range list {
item.TenantId = tenant.Id
item.TenantName = tenant.Name
}
return
}
func updateIMUserStatus(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, state int) error {
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("user-status:%d:%d:%d:%s", scope.TenantId, userId, state, requestId)))
eventKey := hex.EncodeToString(eventKeyBytes[:])
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
current, txErr := tx.GetValue("SELECT state FROM im_users WHERE id=? LIMIT 1", userId)
if txErr != nil {
return txErr
}
if current.IsNil() {
return gerror.New("IM用户不存在")
}
if current.Int() == consts.ImUserStateDeleted {
return gerror.New("已删除账号不能变更状态")
}
if current.Int() == state {
return nil
}
if _, txErr = tx.Exec("UPDATE im_users SET state=?, updatedat=? WHERE id=?", state, now, userId); txErr != nil {
return txErr
}
if _, txErr = tx.Exec(`INSERT INTO im_user_security
(id,user_id,auth_version,created_at,updated_at)
VALUES(?,?,?,?,?)
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
securityId, userId, 2, 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
}
payload, _ := json.Marshal(map[string]any{
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "state": state, "auth_version": version.Int(),
})
_, 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.user_status_changed", string(payload), now, now)
return txErr
})
}
func revokeIMUserSessions(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, eventType string) error {
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("%s:%d:%d:%s", eventType, scope.TenantId, userId, requestId)))
eventKey := hex.EncodeToString(eventKeyBytes[:])
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用户不存在")
}
if _, txErr = tx.Exec(`INSERT INTO im_user_security
(id,user_id,auth_version,created_at,updated_at)
VALUES(?,?,?,?,?)
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,updated_at=VALUES(updated_at)`,
securityId, userId, 2, 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
}
payload, _ := json.Marshal(map[string]any{
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId, "auth_version": version.Int(),
})
_, 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, eventType, string(payload), now, now)
return txErr
})
}
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 {
@@ -29,9 +29,6 @@ type ImAuditLogListInp struct {
}
func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
return
}
@@ -61,9 +58,6 @@ type ImAuditLogViewInp struct {
}
func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
if in.TenantId <= 0 {
return gerror.New("请选择租户")
}
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
@@ -73,3 +67,18 @@ func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
type ImAuditLogViewModel struct {
ImAuditLogListModel
}
type ImAuditLogExportModel struct {
Id int64 `json:"id" dc:"ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
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:"客户端"`
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
}
@@ -155,3 +155,67 @@ func (in *ImTenantStatusInp) Filter(ctx context.Context) (err error) {
}
type ImTenantStatusModel struct{}
type ImTenantUsageInp struct {
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImTenantUsageInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
type ImTenantUsageModel struct {
Id int64 `json:"id" dc:"租户ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
DbName string `json:"dbName" dc:"业务库名"`
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
DbReachable bool `json:"dbReachable" dc:"业务库是否可达"`
DbCheckError string `json:"dbCheckError" dc:"业务库检查错误"`
SchemaVersion string `json:"schemaVersion" dc:"Schema版本"`
UserCount int64 `json:"userCount" dc:"用户数"`
ActiveUserCount int64 `json:"activeUserCount" dc:"正常用户数"`
GroupCount int64 `json:"groupCount" dc:"群数"`
DeviceCount int64 `json:"deviceCount" dc:"设备数"`
PushTokenCount int64 `json:"pushTokenCount" dc:"Push Token数"`
InviteUseCount int64 `json:"inviteUseCount" dc:"邀请码使用次数"`
OutboxPendingCount int64 `json:"outboxPendingCount" dc:"待处理Outbox数"`
Warnings []string `json:"warnings" dc:"警告信息"`
}
type ImTenantDeletionCheckInp struct {
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImTenantDeletionCheckInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
type ImTenantDeletionCheckModel struct {
Id int64 `json:"id" dc:"租户ID"`
Code string `json:"code" dc:"租户编码"`
Name string `json:"name" dc:"租户名称"`
CanEnterDeleting bool `json:"canEnterDeleting" dc:"是否可进入删除流程"`
Blockers []string `json:"blockers" dc:"阻断原因"`
Warnings []string `json:"warnings" dc:"警告信息"`
UserCount int64 `json:"userCount" dc:"用户数"`
GroupCount int64 `json:"groupCount" dc:"群数"`
PendingOutbox int64 `json:"pendingOutbox" dc:"待处理Outbox"`
}
type ImTenantLifecycleInp struct {
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
}
func (in *ImTenantLifecycleInp) Filter(ctx context.Context) (err error) {
if in.Id <= 0 {
return gerror.New("ID不能为空")
}
return
}
@@ -87,6 +87,31 @@ func (in *ImUserPasswordResetInp) Filter(ctx context.Context) error {
return nil
}
type ImUserRevokeSessionsInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
func (in *ImUserRevokeSessionsInp) Filter(ctx context.Context) error {
if _, err := ParseOptionalInt64(in.UserId); err != nil {
return err
}
return nil
}
type ImUserStatusInp struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
State int `json:"state" dc:"账号状态"`
}
func (in *ImUserStatusInp) Filter(ctx context.Context) error {
if _, err := ParseOptionalInt64(in.UserId); err != nil {
return err
}
return nil
}
type ImUserViewModel struct {
ImUserListModel
Signature string `json:"signature" dc:"个性签名"`
@@ -97,3 +122,31 @@ type ImUserViewModel struct {
AccessJson string `json:"accessJson" dc:"访问权限"`
ProfileVersion int `json:"profileVersion" dc:"资料版本"`
}
type ImUserDeviceListInp struct {
form.PageReq
TenantId int64 `json:"tenantId" dc:"租户ID"`
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
}
func (in *ImUserDeviceListInp) Filter(ctx context.Context) (err error) {
if _, err = ParseOptionalInt64(in.UserId); err != nil {
return gerror.New("用户ID不能为空")
}
return
}
type ImUserDeviceListModel struct {
TenantId int64 `json:"tenantId" dc:"租户ID"`
TenantName string `json:"tenantName" dc:"租户名称"`
UserId string `json:"userId" dc:"用户ID"`
DeviceId string `json:"deviceId" dc:"设备ID"`
Platform string `json:"platform" dc:"平台"`
Lang string `json:"lang" dc:"语言"`
LastSeen *gtime.Time `json:"lastSeen" dc:"最后活跃时间"`
PushProvider string `json:"pushProvider" dc:"Push服务商"`
PushPlatform string `json:"pushPlatform" dc:"Push平台"`
PushAppVersion string `json:"pushAppVersion" dc:"App版本"`
PushTokenConfigured bool `json:"pushTokenConfigured" dc:"是否配置Push Token"`
PushUpdatedAt *gtime.Time `json:"pushUpdatedAt" dc:"Push更新时间"`
}
+16
View File
@@ -182,6 +182,14 @@ type (
View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error)
// Status 更新IM租户状态
Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error)
// Usage 获取IM租户用量和健康状态
Usage(ctx context.Context, in *sysin.ImTenantUsageInp) (res *sysin.ImTenantUsageModel, err error)
// DeletionCheck IM租户删除前检查
DeletionCheck(ctx context.Context, in *sysin.ImTenantDeletionCheckInp) (res *sysin.ImTenantDeletionCheckModel, err error)
// EnterDeleting IM租户进入删除流程
EnterDeleting(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error)
// Restore 恢复IM租户
Restore(ctx context.Context, in *sysin.ImTenantLifecycleInp) (err error)
}
ISysImRegistration interface {
// GetConfig 获取IM注册配置
@@ -212,6 +220,12 @@ type (
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
// ResetPassword 管理员重置IM用户密码
ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (err error)
// RevokeSessions 撤销IM用户会话
RevokeSessions(ctx context.Context, in *sysin.ImUserRevokeSessionsInp) (err error)
// Status 更新IM用户状态
Status(ctx context.Context, in *sysin.ImUserStatusInp) (err error)
// Devices 获取IM用户设备列表
Devices(ctx context.Context, in *sysin.ImUserDeviceListInp) (list []*sysin.ImUserDeviceListModel, totalCount int, err error)
}
ISysImGroup interface {
// List 获取IM群组列表
@@ -252,6 +266,8 @@ type (
ISysImAuditLog interface {
// List 获取IM审计日志列表
List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error)
// Export 导出IM审计日志列表
Export(ctx context.Context, in *sysin.ImAuditLogListInp) (err error)
// View 获取IM审计日志指定信息
View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error)
}
@@ -27,3 +27,7 @@ SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @auditListId, '审计详情', 'imAuditLogView', '', '', '3', '', '/imAuditLog/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' tr_', @auditListId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAuditLogView');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @auditListId, '导出审计日志', 'imAuditLogExport', '', '', '3', '', '/imAuditLog/export', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' tr_', @auditListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAuditLogExport');
@@ -35,3 +35,19 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantEdit');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @tenantListId, '修改租户状态', 'imTenantStatus', '', '', '3', '', '/imTenant/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantStatus');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @tenantListId, '租户用量与健康', 'imTenantUsage', '', '', '3', '', '/imTenant/usage', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantUsage');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @tenantListId, '租户删除前检查', 'imTenantDeletionCheck', '', '', '3', '', '/imTenant/deletionCheck', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantDeletionCheck');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @tenantListId, '租户进入删除流程', 'imTenantEnterDeleting', '', '', '3', '', '/imTenant/enterDeleting', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '60', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantEnterDeleting');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @tenantListId, '恢复租户', 'imTenantRestore', '', '', '3', '', '/imTenant/restore', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '70', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantRestore');
@@ -31,3 +31,15 @@ WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '重置用户密码', 'imUserResetPassword', '', '', '3', '', '/imUser/resetPassword', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserResetPassword');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '用户设备列表', 'imUserDevices', '', '', '3', '', '/imUser/devices', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserDevices');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '撤销用户会话', 'imUserRevokeSessions', '', '', '3', '', '/imUser/revokeSessions', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserRevokeSessions');
INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @userListId, '更新用户状态', 'imUserStatus', '', '', '3', '', '/imUser/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserStatus');
@@ -139,6 +139,25 @@ WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imRegistrationInvite
SET @invitePage := (SELECT id FROM hg_admin_menu WHERE name = 'imRegistrationInviteIndex' LIMIT 1);
UPDATE hg_admin_menu SET tree = CONCAT('tr_', id, ' tr_', @imRoot, ' ') WHERE id = @invitePage;
SET @imTenantPage := (SELECT id FROM hg_admin_menu WHERE name = 'imTenantIndex' LIMIT 1);
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @imTenantPage, 4, CONCAT('tr_', @imTenantPage, ' '), '租户用量与健康', 'imTenantUsage', '', '', 3,
'/imTenant/usage', '', 1, 40, 1, NOW(), NOW()
WHERE @imTenantPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imTenantUsage');
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @imTenantPage, 4, CONCAT('tr_', @imTenantPage, ' '), title, name, '', '', 3, permissions, '', 1, sort, 1, NOW(), NOW()
FROM (
SELECT '租户删除前检查' title, 'imTenantDeletionCheck' name, '/imTenant/deletionCheck' permissions, 50 sort UNION ALL
SELECT '租户进入删除流程', 'imTenantEnterDeleting', '/imTenant/enterDeleting', 60 UNION ALL
SELECT '恢复租户', 'imTenantRestore', '/imTenant/restore', 70
) p
WHERE @imTenantPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, sort, status, created_at, updated_at)
SELECT @invitePage, 3, CONCAT('tr_', @invitePage, ' tr_', @imRoot, ' '), title, name, '', '', 3, permissions, '', sort, 1, NOW(), NOW()
@@ -152,6 +171,17 @@ FROM (
) p
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
SET @systemConfigPage := (SELECT id FROM hg_admin_menu WHERE name = 'system_config' LIMIT 1);
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @systemConfigPage, 3, CONCAT('tr_', @systemConfigPage, ' '), title, name, '', '', 3, permissions, '', 1, sort, 1, NOW(), NOW()
FROM (
SELECT '查看IM注册配置' title, 'imRegistrationConfigGet' name, '/imRegistration/config/get' permissions, 80 sort UNION ALL
SELECT '更新IM注册配置', 'imRegistrationConfigUpdate', '/imRegistration/config/update', 90
) p
WHERE @systemConfigPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
SET @imUserPage := (SELECT id FROM hg_admin_menu WHERE name = 'imUserIndex' LIMIT 1);
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
@@ -159,3 +189,24 @@ SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '重置用户密码', 'i
'/imUser/resetPassword', '', 1, 20, 1, NOW(), NOW()
WHERE @imUserPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserResetPassword');
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '用户设备列表', 'imUserDevices', '', '', 3,
'/imUser/devices', '', 1, 30, 1, NOW(), NOW()
WHERE @imUserPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserDevices');
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '撤销用户会话', 'imUserRevokeSessions', '', '', 3,
'/imUser/revokeSessions', '', 1, 40, 1, NOW(), NOW()
WHERE @imUserPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserRevokeSessions');
INSERT INTO hg_admin_menu
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '更新用户状态', 'imUserStatus', '', '', 3,
'/imUser/status', '', 1, 50, 1, NOW(), NOW()
WHERE @imUserPage IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserStatus');