feat: expand IM admin controls
This commit is contained in:
@@ -22,6 +22,14 @@ type ListRes struct {
|
|||||||
List []*sysin.ImAuditLogListModel `json:"list" dc:"数据列表"`
|
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审计日志指定信息
|
// ViewReq 获取IM审计日志指定信息
|
||||||
type ViewReq struct {
|
type ViewReq struct {
|
||||||
g.Meta `path:"/imAuditLog/view" method:"get" tags:"IM审计日志" summary:"获取IM审计日志指定信息"`
|
g.Meta `path:"/imAuditLog/view" method:"get" tags:"IM审计日志" summary:"获取IM审计日志指定信息"`
|
||||||
|
|||||||
@@ -48,3 +48,35 @@ type StatusReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StatusRes 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{}
|
||||||
|
|||||||
@@ -39,3 +39,27 @@ type ResetPasswordReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResetPasswordRes 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
|
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审计日志信息
|
// View 获取指定IM审计日志信息
|
||||||
func (c *cImAuditLog) View(ctx context.Context, req *imauditlog.ViewReq) (res *imauditlog.ViewRes, err error) {
|
func (c *cImAuditLog) View(ctx context.Context, req *imauditlog.ViewReq) (res *imauditlog.ViewRes, err error) {
|
||||||
data, err := service.SysImAuditLog().View(ctx, &req.ImAuditLogViewInp)
|
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)
|
err = service.SysImTenant().Status(ctx, &req.ImTenantStatusInp)
|
||||||
return
|
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)
|
err = service.SysImUser().ResetPassword(ctx, &req.ImUserPasswordResetInp)
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
|
"hotgo/internal/model/input/form"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
|
"hotgo/utility/convert"
|
||||||
|
"hotgo/utility/excel"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
"github.com/gogf/gf/v2/util/gconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sSysImAuditLog struct{}
|
type sSysImAuditLog struct{}
|
||||||
@@ -30,7 +36,11 @@ func (s *sSysImAuditLog) List(ctx context.Context, in *sysin.ImAuditLogListInp)
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -107,7 +117,11 @@ func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp)
|
|||||||
if err = in.Filter(ctx); err != nil {
|
if err = in.Filter(ctx); err != nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -123,3 +137,23 @@ func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp)
|
|||||||
res.TenantName = tenant.Name
|
res.TenantName = tenant.Name
|
||||||
return
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ package sys
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/dao"
|
"hotgo/internal/dao"
|
||||||
"hotgo/internal/library/contexts"
|
"hotgo/internal/library/contexts"
|
||||||
"hotgo/internal/library/hgorm/handler"
|
"hotgo/internal/library/hgorm/handler"
|
||||||
"hotgo/internal/library/hgorm/hook"
|
"hotgo/internal/library/hgorm/hook"
|
||||||
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/entity"
|
"hotgo/internal/model/entity"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
@@ -272,3 +275,137 @@ func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp)
|
|||||||
}
|
}
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hotgo/internal/consts"
|
||||||
"hotgo/internal/library/imtenant"
|
"hotgo/internal/library/imtenant"
|
||||||
"hotgo/internal/model/input/sysin"
|
"hotgo/internal/model/input/sysin"
|
||||||
"hotgo/internal/service"
|
"hotgo/internal/service"
|
||||||
@@ -248,6 +249,213 @@ func (s *sSysImUser) ResetPassword(ctx context.Context, in *sysin.ImUserPassword
|
|||||||
return err
|
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 {
|
func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, newPassword string) error {
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ type ImAuditLogListInp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) {
|
func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) {
|
||||||
if in.TenantId <= 0 {
|
|
||||||
return gerror.New("请选择租户")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +58,6 @@ type ImAuditLogViewInp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
|
func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
|
||||||
if in.TenantId <= 0 {
|
|
||||||
return gerror.New("请选择租户")
|
|
||||||
}
|
|
||||||
if in.Id <= 0 {
|
if in.Id <= 0 {
|
||||||
return gerror.New("ID不能为空")
|
return gerror.New("ID不能为空")
|
||||||
}
|
}
|
||||||
@@ -73,3 +67,18 @@ func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
|
|||||||
type ImAuditLogViewModel struct {
|
type ImAuditLogViewModel struct {
|
||||||
ImAuditLogListModel
|
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 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
|
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 {
|
type ImUserViewModel struct {
|
||||||
ImUserListModel
|
ImUserListModel
|
||||||
Signature string `json:"signature" dc:"个性签名"`
|
Signature string `json:"signature" dc:"个性签名"`
|
||||||
@@ -97,3 +122,31 @@ type ImUserViewModel struct {
|
|||||||
AccessJson string `json:"accessJson" dc:"访问权限"`
|
AccessJson string `json:"accessJson" dc:"访问权限"`
|
||||||
ProfileVersion int `json:"profileVersion" 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更新时间"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -182,6 +182,14 @@ type (
|
|||||||
View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error)
|
View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error)
|
||||||
// Status 更新IM租户状态
|
// Status 更新IM租户状态
|
||||||
Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error)
|
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 {
|
ISysImRegistration interface {
|
||||||
// GetConfig 获取IM注册配置
|
// GetConfig 获取IM注册配置
|
||||||
@@ -212,6 +220,12 @@ type (
|
|||||||
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
|
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
|
||||||
// ResetPassword 管理员重置IM用户密码
|
// ResetPassword 管理员重置IM用户密码
|
||||||
ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (err error)
|
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 {
|
ISysImGroup interface {
|
||||||
// List 获取IM群组列表
|
// List 获取IM群组列表
|
||||||
@@ -252,6 +266,8 @@ type (
|
|||||||
ISysImAuditLog interface {
|
ISysImAuditLog interface {
|
||||||
// List 获取IM审计日志列表
|
// List 获取IM审计日志列表
|
||||||
List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error)
|
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 获取IM审计日志指定信息
|
||||||
View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error)
|
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`)
|
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
|
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');
|
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`)
|
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
|
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');
|
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`)
|
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
|
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');
|
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);
|
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;
|
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
|
INSERT INTO hg_admin_menu
|
||||||
(pid, level, tree, title, name, path, icon, type, permissions, component, sort, status, created_at, updated_at)
|
(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()
|
SELECT @invitePage, 3, CONCAT('tr_', @invitePage, ' tr_', @imRoot, ' '), title, name, '', '', 3, permissions, '', sort, 1, NOW(), NOW()
|
||||||
@@ -152,6 +171,17 @@ FROM (
|
|||||||
) p
|
) p
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
|
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);
|
SET @imUserPage := (SELECT id FROM hg_admin_menu WHERE name = 'imUserIndex' LIMIT 1);
|
||||||
INSERT INTO hg_admin_menu
|
INSERT INTO hg_admin_menu
|
||||||
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
|
(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()
|
'/imUser/resetPassword', '', 1, 20, 1, NOW(), NOW()
|
||||||
WHERE @imUserPage IS NOT NULL
|
WHERE @imUserPage IS NOT NULL
|
||||||
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserResetPassword');
|
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');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { http } from '@/utils/http/axios';
|
import { http, jumpExport } from '@/utils/http/axios';
|
||||||
|
|
||||||
// 获取IM审计日志列表
|
// 获取IM审计日志列表
|
||||||
export function List(params) {
|
export function List(params) {
|
||||||
@@ -17,3 +17,8 @@ export function View(params) {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出IM审计日志
|
||||||
|
export function Export(params) {
|
||||||
|
jumpExport('/imAuditLog/export', params);
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,3 +35,36 @@ export function View(params) {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取IM租户用量和健康状态
|
||||||
|
export function Usage(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imTenant/usage',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeletionCheck(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imTenant/deletionCheck',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnterDeleting(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imTenant/enterDeleting',
|
||||||
|
method: 'POST',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Restore(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imTenant/restore',
|
||||||
|
method: 'POST',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,3 +26,30 @@ export function ResetPassword(params) {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 撤销IM用户会话
|
||||||
|
export function RevokeSessions(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imUser/revokeSessions',
|
||||||
|
method: 'POST',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新IM用户状态
|
||||||
|
export function Status(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imUser/status',
|
||||||
|
method: 'POST',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取IM用户设备列表
|
||||||
|
export function Devices(params) {
|
||||||
|
return http.request({
|
||||||
|
url: '/imUser/devices',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="n-layout-page-header">
|
<div class="n-layout-page-header">
|
||||||
<n-card :bordered="false" title="IM注册邀请码"></n-card>
|
<n-card :bordered="false" title="IM注册邀请码" />
|
||||||
</div>
|
</div>
|
||||||
<n-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicForm
|
<BasicForm
|
||||||
@@ -22,13 +22,23 @@
|
|||||||
>
|
>
|
||||||
<template #tableTitle>
|
<template #tableTitle>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" @click="handleCreate(false)" v-if="hasPermission(['/imInvite/create'])">
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleCreate(false)"
|
||||||
|
v-if="hasPermission(['/imInvite/create'])"
|
||||||
|
>
|
||||||
添加
|
添加
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button type="primary" @click="handleCreate(true)" v-if="hasPermission(['/imInvite/batchCreate'])">
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleCreate(true)"
|
||||||
|
v-if="hasPermission(['/imInvite/batchCreate'])"
|
||||||
|
>
|
||||||
批量生成
|
批量生成
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="handleExport" v-if="hasPermission(['/imInvite/export'])">导出</n-button>
|
<n-button @click="handleExport" v-if="hasPermission(['/imInvite/export'])">
|
||||||
|
导出
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
@@ -45,7 +55,7 @@
|
|||||||
import { BasicForm, useForm } from '@/components/Form/index';
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import { Export, List, Revoke } from '@/api/im/registrationInvite';
|
import { BatchRevoke, Export, List, Revoke } from '@/api/im/registrationInvite';
|
||||||
import {
|
import {
|
||||||
columns,
|
columns,
|
||||||
loadOptions,
|
loadOptions,
|
||||||
@@ -88,7 +98,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 220,
|
width: 300,
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
@@ -109,6 +119,13 @@
|
|||||||
auth: ['/imInvite/revoke'],
|
auth: ['/imInvite/revoke'],
|
||||||
ifShow: () => record.status === 'active',
|
ifShow: () => record.status === 'active',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '撤销批次',
|
||||||
|
type: 'error',
|
||||||
|
onClick: handleBatchRevoke.bind(null, record),
|
||||||
|
auth: ['/imInvite/batchRevoke'],
|
||||||
|
ifShow: () => !!record.batchId && record.status === 'active',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -165,6 +182,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBatchRevoke(record: State) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认撤销批次',
|
||||||
|
content: `将撤销批次 ${record.batchId} 下仍有效的邀请码,已过期或已撤销的邀请码不受影响,是否确认?`,
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await BatchRevoke({
|
||||||
|
tenantId: Number(record.tenantId || selectedTenantId()),
|
||||||
|
batchId: record.batchId,
|
||||||
|
});
|
||||||
|
message.success('批次撤销成功');
|
||||||
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
const params = { ...searchFormRef.value?.formModel };
|
const params = { ...searchFormRef.value?.formModel };
|
||||||
const response: any = await Export(params);
|
const response: any = await Export(params);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="n-layout-page-header">
|
<div class="n-layout-page-header">
|
||||||
<n-card :bordered="false" title="IM审计日志"></n-card>
|
<n-card :bordered="false" title="IM审计日志" />
|
||||||
</div>
|
</div>
|
||||||
<n-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicForm
|
<BasicForm
|
||||||
@@ -19,7 +19,23 @@
|
|||||||
:actionColumn="actionColumn"
|
:actionColumn="actionColumn"
|
||||||
:scroll-x="scrollX"
|
:scroll-x="scrollX"
|
||||||
:resizeHeightOffset="-10000"
|
:resizeHeightOffset="-10000"
|
||||||
/>
|
>
|
||||||
|
<template #tableTitle>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
class="min-left-space"
|
||||||
|
@click="handleExport"
|
||||||
|
v-if="hasPermission(['/imAuditLog/export'])"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ExportOutlined />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</BasicTable>
|
||||||
</n-card>
|
</n-card>
|
||||||
<ViewModal ref="viewRef" />
|
<ViewModal ref="viewRef" />
|
||||||
</div>
|
</div>
|
||||||
@@ -30,16 +46,27 @@
|
|||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';
|
import { BasicTable, TableAction } from '@/components/Table';
|
||||||
import { BasicForm, useForm } from '@/components/Form/index';
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { List } from '@/api/imAuditLog';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import { List, Export } from '@/api/imAuditLog';
|
||||||
import { columns, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
import { columns, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
||||||
import { adaTableScrollX } from '@/utils/hotgo';
|
import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
|
import { ExportOutlined } from '@vicons/antd';
|
||||||
import ViewModal from './view.vue';
|
import ViewModal from './view.vue';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
||||||
const actionRef = ref();
|
const actionRef = ref();
|
||||||
const searchFormRef = ref<any>({});
|
const searchFormRef = ref<any>({});
|
||||||
const viewRef = ref();
|
const viewRef = ref();
|
||||||
|
|
||||||
|
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
||||||
|
if (tenantSchema) {
|
||||||
|
tenantSchema.ifShow = () => isPlatformAdmin.value;
|
||||||
|
}
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 100,
|
width: 100,
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -48,7 +75,13 @@
|
|||||||
render(record: State) {
|
render(record: State) {
|
||||||
return h(TableAction as any, {
|
return h(TableAction as any, {
|
||||||
style: 'button',
|
style: 'button',
|
||||||
actions: [{ label: '详情', onClick: () => viewRef.value.openModal(record), auth: ['/imAuditLog/view'] }],
|
actions: [
|
||||||
|
{
|
||||||
|
label: '详情',
|
||||||
|
onClick: () => viewRef.value.openModal(record),
|
||||||
|
auth: ['/imAuditLog/view'],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -63,7 +96,8 @@
|
|||||||
|
|
||||||
const loadDataTable = async (res) => {
|
const loadDataTable = async (res) => {
|
||||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||||
if (!params.tenantId) return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
if (isPlatformAdmin.value && !params.tenantId)
|
||||||
|
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||||
return await List(params);
|
return await List(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,19 +107,31 @@
|
|||||||
|
|
||||||
async function handleReset() {
|
async function handleReset() {
|
||||||
const defaults: Recordable = {};
|
const defaults: Recordable = {};
|
||||||
if (tenantOptions.value.length > 0) defaults.tenantId = tenantOptions.value[0].value;
|
if (isPlatformAdmin.value && tenantOptions.value.length > 0)
|
||||||
|
defaults.tenantId = tenantOptions.value[0].value;
|
||||||
await setFieldsValue(defaults);
|
await setFieldsValue(defaults);
|
||||||
reloadTable();
|
reloadTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
function handleExport() {
|
||||||
await loadTenantOptions();
|
const params = { ...searchFormRef.value?.formModel };
|
||||||
if (tenantOptions.value.length > 0) {
|
if (isPlatformAdmin.value && !params.tenantId) {
|
||||||
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
|
message.warning('请选择租户');
|
||||||
reloadTable();
|
return;
|
||||||
} else {
|
|
||||||
message.warning('暂无可用租户');
|
|
||||||
}
|
}
|
||||||
|
Export(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (isPlatformAdmin.value) {
|
||||||
|
await loadTenantOptions();
|
||||||
|
if (tenantOptions.value.length > 0) {
|
||||||
|
await setFieldsValue({ tenantId: tenantOptions.value[0].value });
|
||||||
|
} else {
|
||||||
|
message.warning('暂无可用租户');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reloadTable();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="dialog"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="'删除前检查 #' + current.id"
|
||||||
|
:mask-closable="false"
|
||||||
|
:style="{ width: dialogWidth }"
|
||||||
|
>
|
||||||
|
<n-spin :show="loading" description="请稍候...">
|
||||||
|
<n-alert :type="detail.canEnterDeleting ? 'success' : 'warning'" class="mb-4">
|
||||||
|
{{
|
||||||
|
detail.canEnterDeleting ? '检查通过,可进入删除流程。' : '检查未通过,不能进入删除流程。'
|
||||||
|
}}
|
||||||
|
</n-alert>
|
||||||
|
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||||
|
<n-descriptions-item label="租户">{{ detail.name || current.name }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="编码">{{ detail.code || current.code }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="用户数">{{ detail.userCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="群数">{{ detail.groupCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="待处理Outbox">{{ detail.pendingOutbox }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item v-if="detail.blockers?.length" label="阻断项" :span="2">
|
||||||
|
<n-code :code="detail.blockers.join('\n')" language="text" word-wrap />
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item v-if="detail.warnings?.length" label="警告" :span="2">
|
||||||
|
<n-code :code="detail.warnings.join('\n')" language="text" word-wrap />
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-spin>
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="load">重新检查</n-button>
|
||||||
|
<n-button
|
||||||
|
type="error"
|
||||||
|
:disabled="!detail.canEnterDeleting"
|
||||||
|
:loading="submitLoading"
|
||||||
|
@click="enterDeleting"
|
||||||
|
>
|
||||||
|
进入删除流程
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showModal = false">关闭</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { useMessage } from 'naive-ui';
|
||||||
|
import { DeletionCheck, EnterDeleting } from '@/api/imTenant';
|
||||||
|
import { newState, State } from './model';
|
||||||
|
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||||
|
import { adaModalWidth } from '@/utils/hotgo';
|
||||||
|
|
||||||
|
const emit = defineEmits(['reload']);
|
||||||
|
const message = useMessage();
|
||||||
|
const settingStore = useProjectSettingStore();
|
||||||
|
const showModal = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const current = ref<State>(newState(null));
|
||||||
|
const dialogWidth = computed(() => adaModalWidth(840));
|
||||||
|
const detail = reactive({
|
||||||
|
id: 0,
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
canEnterDeleting: false,
|
||||||
|
blockers: [] as string[],
|
||||||
|
warnings: [] as string[],
|
||||||
|
userCount: 0,
|
||||||
|
groupCount: 0,
|
||||||
|
pendingOutbox: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
Object.assign(detail, await DeletionCheck({ id: current.value.id }));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterDeleting() {
|
||||||
|
submitLoading.value = true;
|
||||||
|
try {
|
||||||
|
await EnterDeleting({ id: current.value.id });
|
||||||
|
message.success('已进入删除流程');
|
||||||
|
showModal.value = false;
|
||||||
|
emit('reload');
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(state: State) {
|
||||||
|
current.value = newState(state);
|
||||||
|
showModal.value = true;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ openModal });
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="n-layout-page-header">
|
<div class="n-layout-page-header">
|
||||||
<n-card :bordered="false" title="IM租户管理"></n-card>
|
<n-card :bordered="false" title="IM租户管理" />
|
||||||
</div>
|
</div>
|
||||||
<n-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicForm
|
<BasicForm
|
||||||
@@ -37,7 +37,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</BasicTable>
|
</BasicTable>
|
||||||
</n-card>
|
</n-card>
|
||||||
<Edit ref="editRef" @reloadTable="reloadTable" />
|
<Edit ref="editRef" @reload-table="reloadTable" />
|
||||||
|
<UsageModal ref="usageRef" />
|
||||||
|
<DeletionCheckModal ref="deletionCheckRef" @reload="reloadTable" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -48,11 +50,13 @@
|
|||||||
import { BasicForm, useForm } from '@/components/Form/index';
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { useDictStore } from '@/store/modules/dict';
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
import { List, Status } from '@/api/imTenant';
|
import { List, Restore, Status } from '@/api/imTenant';
|
||||||
import { PlusOutlined } from '@vicons/antd';
|
import { PlusOutlined } from '@vicons/antd';
|
||||||
import { columns, loadOptions, schemas, State } from './model';
|
import { columns, loadOptions, schemas, State } from './model';
|
||||||
import { adaTableScrollX } from '@/utils/hotgo';
|
import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
import Edit from './edit.vue';
|
import Edit from './edit.vue';
|
||||||
|
import UsageModal from './usage.vue';
|
||||||
|
import DeletionCheckModal from './deletionCheck.vue';
|
||||||
|
|
||||||
const dict = useDictStore();
|
const dict = useDictStore();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
@@ -60,9 +64,11 @@
|
|||||||
const actionRef = ref();
|
const actionRef = ref();
|
||||||
const searchFormRef = ref<any>({});
|
const searchFormRef = ref<any>({});
|
||||||
const editRef = ref();
|
const editRef = ref();
|
||||||
|
const usageRef = ref();
|
||||||
|
const deletionCheckRef = ref();
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 180,
|
width: 240,
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
@@ -75,6 +81,11 @@
|
|||||||
onClick: handleEdit.bind(null, record),
|
onClick: handleEdit.bind(null, record),
|
||||||
auth: ['/imTenant/edit'],
|
auth: ['/imTenant/edit'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '用量',
|
||||||
|
onClick: handleUsage.bind(null, record),
|
||||||
|
auth: ['/imTenant/usage'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '停用',
|
label: '停用',
|
||||||
onClick: handleStatus.bind(null, record, 2),
|
onClick: handleStatus.bind(null, record, 2),
|
||||||
@@ -84,9 +95,22 @@
|
|||||||
{
|
{
|
||||||
label: '启用',
|
label: '启用',
|
||||||
onClick: handleStatus.bind(null, record, 1),
|
onClick: handleStatus.bind(null, record, 1),
|
||||||
ifShow: () => record.state !== 1,
|
ifShow: () => ![1, 3, 4].includes(record.state),
|
||||||
auth: ['/imTenant/status'],
|
auth: ['/imTenant/status'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '恢复',
|
||||||
|
onClick: handleRestore.bind(null, record),
|
||||||
|
ifShow: () => [2, 3].includes(record.state),
|
||||||
|
auth: ['/imTenant/restore'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '删除检查',
|
||||||
|
type: 'error',
|
||||||
|
onClick: handleDeletionCheck.bind(null, record),
|
||||||
|
ifShow: () => record.state !== 4,
|
||||||
|
auth: ['/imTenant/deletionCheck'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -122,6 +146,21 @@
|
|||||||
editRef.value.openModal(record);
|
editRef.value.openModal(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUsage(record: State) {
|
||||||
|
usageRef.value.openModal(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeletionCheck(record: State) {
|
||||||
|
deletionCheckRef.value.openModal(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRestore(record: State) {
|
||||||
|
Restore({ id: record.id }).then((_res) => {
|
||||||
|
message.success('恢复成功');
|
||||||
|
reloadTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 修改状态
|
// 修改状态
|
||||||
function handleStatus(record: Recordable, state: number) {
|
function handleStatus(record: Recordable, state: number) {
|
||||||
Status({ id: record.id, state }).then((_res) => {
|
Status({ id: record.id, state }).then((_res) => {
|
||||||
|
|||||||
@@ -34,6 +34,25 @@ export class State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UsageState {
|
||||||
|
public id = 0;
|
||||||
|
public code = '';
|
||||||
|
public name = '';
|
||||||
|
public dbName = '';
|
||||||
|
public dbDsnConfigured = false;
|
||||||
|
public dbReachable = false;
|
||||||
|
public dbCheckError = '';
|
||||||
|
public schemaVersion = '';
|
||||||
|
public userCount = 0;
|
||||||
|
public activeUserCount = 0;
|
||||||
|
public groupCount = 0;
|
||||||
|
public deviceCount = 0;
|
||||||
|
public pushTokenCount = 0;
|
||||||
|
public inviteUseCount = 0;
|
||||||
|
public outboxPendingCount = 0;
|
||||||
|
public warnings: string[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
export function newState(state: State | Record<string, any> | null): State {
|
export function newState(state: State | Record<string, any> | null): State {
|
||||||
if (state !== null) {
|
if (state !== null) {
|
||||||
if (state instanceof State) {
|
if (state instanceof State) {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="dialog"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="'租户健康与用量 #' + current.id"
|
||||||
|
:mask-closable="false"
|
||||||
|
:style="{ width: dialogWidth }"
|
||||||
|
>
|
||||||
|
<n-spin :show="loading" description="请稍候...">
|
||||||
|
<n-descriptions label-placement="left" bordered :column="settingStore.isMobile ? 1 : 2">
|
||||||
|
<n-descriptions-item label="租户">{{ usage.name || current.name }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="租户编码">{{ usage.code || current.code }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="业务库">{{ usage.dbName || '-' }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="独立连接串">
|
||||||
|
{{ usage.dbDsnConfigured ? '已配置' : '未配置' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="业务库状态">
|
||||||
|
<n-tag :type="usage.dbReachable ? 'success' : 'error'" :bordered="false">
|
||||||
|
{{ usage.dbReachable ? '可达' : '不可达' }}
|
||||||
|
</n-tag>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="Schema版本">
|
||||||
|
{{ usage.schemaVersion || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="用户数">{{ usage.userCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="正常用户数">{{ usage.activeUserCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="群数">{{ usage.groupCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="设备数">{{ usage.deviceCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="Push Token">{{ usage.pushTokenCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="邀请码使用">{{ usage.inviteUseCount }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="待处理Outbox">
|
||||||
|
{{ usage.outboxPendingCount }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item v-if="usage.dbCheckError" label="连接错误" :span="2">
|
||||||
|
{{ usage.dbCheckError }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item v-if="usage.warnings?.length" label="检查警告" :span="2">
|
||||||
|
<n-code :code="usage.warnings.join('\n')" language="text" word-wrap />
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-spin>
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="load">刷新</n-button>
|
||||||
|
<n-button @click="showModal = false">关闭</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { Usage } from '@/api/imTenant';
|
||||||
|
import { newState, State, UsageState } from './model';
|
||||||
|
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||||
|
import { adaModalWidth } from '@/utils/hotgo';
|
||||||
|
|
||||||
|
const settingStore = useProjectSettingStore();
|
||||||
|
const showModal = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const current = ref<State>(newState(null));
|
||||||
|
const usage = ref<UsageState>(new UsageState());
|
||||||
|
const dialogWidth = computed(() => adaModalWidth(900));
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
usage.value = Object.assign(new UsageState(), await Usage({ id: current.value.id }));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(state: State) {
|
||||||
|
current.value = newState(state);
|
||||||
|
usage.value = new UsageState();
|
||||||
|
showModal.value = true;
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ openModal });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
preset="dialog"
|
||||||
|
:show-icon="false"
|
||||||
|
:title="'登录设备 ' + userId"
|
||||||
|
:mask-closable="false"
|
||||||
|
:style="{ width: dialogWidth }"
|
||||||
|
>
|
||||||
|
<BasicTable
|
||||||
|
ref="actionRef"
|
||||||
|
:columns="deviceColumns"
|
||||||
|
:request="loadDataTable"
|
||||||
|
:row-key="(row) => row.deviceId"
|
||||||
|
:scroll-x="scrollX"
|
||||||
|
:resizeHeightOffset="-10000"
|
||||||
|
/>
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="showModal = false">关闭</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { BasicTable } from '@/components/Table';
|
||||||
|
import { Devices } from '@/api/imUser';
|
||||||
|
import { deviceColumns, State } from './model';
|
||||||
|
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const actionRef = ref();
|
||||||
|
const tenantId = ref(0);
|
||||||
|
const userId = ref('');
|
||||||
|
const dialogWidth = computed(() => adaModalWidth(1080));
|
||||||
|
const scrollX = computed(() => adaTableScrollX(deviceColumns, 0));
|
||||||
|
|
||||||
|
const loadDataTable = async (res) => {
|
||||||
|
if (!tenantId.value || !userId.value) {
|
||||||
|
return { list: [], page: 1, pageCount: 0, totalCount: 0 };
|
||||||
|
}
|
||||||
|
return await Devices({ tenantId: tenantId.value, userId: userId.value, ...res });
|
||||||
|
};
|
||||||
|
|
||||||
|
function openModal(record: State) {
|
||||||
|
tenantId.value = record.tenantId;
|
||||||
|
userId.value = record.userId;
|
||||||
|
showModal.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
actionRef.value?.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ openModal });
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="n-layout-page-header">
|
<div class="n-layout-page-header">
|
||||||
<n-card :bordered="false" title="IM用户管理"></n-card>
|
<n-card :bordered="false" title="IM用户管理" />
|
||||||
</div>
|
</div>
|
||||||
<n-card :bordered="false" class="proCard">
|
<n-card :bordered="false" class="proCard">
|
||||||
<BasicForm
|
<BasicForm
|
||||||
@@ -22,30 +22,34 @@
|
|||||||
/>
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
<ViewModal ref="viewRef" />
|
<ViewModal ref="viewRef" />
|
||||||
|
<DevicesModal ref="devicesRef" />
|
||||||
<ResetPasswordModal ref="resetPasswordRef" @reload="reloadTable" />
|
<ResetPasswordModal ref="resetPasswordRef" @reload="reloadTable" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useDialog, useMessage } from 'naive-ui';
|
||||||
import { BasicTable, TableAction } from '@/components/Table';
|
import { BasicTable, TableAction } from '@/components/Table';
|
||||||
import { BasicForm, useForm } from '@/components/Form/index';
|
import { BasicForm, useForm } from '@/components/Form/index';
|
||||||
import { usePermission } from '@/hooks/web/usePermission';
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
import { List } from '@/api/imUser';
|
import { List, RevokeSessions } from '@/api/imUser';
|
||||||
import { columns, loadOptions, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
import { columns, loadOptions, loadTenantOptions, schemas, State, tenantOptions } from './model';
|
||||||
import { adaTableScrollX } from '@/utils/hotgo';
|
import { adaTableScrollX } from '@/utils/hotgo';
|
||||||
import ViewModal from './view.vue';
|
import ViewModal from './view.vue';
|
||||||
|
import DevicesModal from './DevicesModal.vue';
|
||||||
import ResetPasswordModal from './ResetPasswordModal.vue';
|
import ResetPasswordModal from './ResetPasswordModal.vue';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
||||||
const actionRef = ref();
|
const actionRef = ref();
|
||||||
const searchFormRef = ref<any>({});
|
const searchFormRef = ref<any>({});
|
||||||
const viewRef = ref();
|
const viewRef = ref();
|
||||||
|
const devicesRef = ref();
|
||||||
const resetPasswordRef = ref();
|
const resetPasswordRef = ref();
|
||||||
|
|
||||||
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
const tenantSchema = schemas.value.find((item) => item.field === 'tenantId');
|
||||||
@@ -54,7 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actionColumn = reactive({
|
const actionColumn = reactive({
|
||||||
width: 220,
|
width: 330,
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
@@ -67,6 +71,17 @@
|
|||||||
onClick: handleView.bind(null, record),
|
onClick: handleView.bind(null, record),
|
||||||
auth: ['/imUser/view'],
|
auth: ['/imUser/view'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '设备',
|
||||||
|
onClick: handleDevices.bind(null, record),
|
||||||
|
auth: ['/imUser/devices'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '撤销会话',
|
||||||
|
type: 'error',
|
||||||
|
onClick: handleRevokeSessions.bind(null, record),
|
||||||
|
auth: ['/imUser/revokeSessions'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '重置密码',
|
label: '重置密码',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@@ -107,6 +122,24 @@
|
|||||||
viewRef.value.openModal(record);
|
viewRef.value.openModal(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDevices(record: State) {
|
||||||
|
devicesRef.value.openModal(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRevokeSessions(record: State) {
|
||||||
|
dialog.warning({
|
||||||
|
title: '撤销用户会话',
|
||||||
|
content: '将递增该用户认证版本,并通知 IM 服务清退在线会话,是否确认?',
|
||||||
|
positiveText: '确认',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await RevokeSessions({ tenantId: record.tenantId, userId: record.userId });
|
||||||
|
message.success('会话撤销事件已提交');
|
||||||
|
reloadTable();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleResetPassword(record: State) {
|
function handleResetPassword(record: State) {
|
||||||
resetPasswordRef.value.openModal(record);
|
resetPasswordRef.value.openModal(record);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, h, ref } from 'vue';
|
||||||
|
import { NSwitch } from 'naive-ui';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { FormSchema } from '@/components/Form';
|
import { FormSchema } from '@/components/Form';
|
||||||
import { defRangeShortcuts } from '@/utils/dateUtil';
|
import { defRangeShortcuts } from '@/utils/dateUtil';
|
||||||
import { useDictStore } from '@/store/modules/dict';
|
import { useDictStore } from '@/store/modules/dict';
|
||||||
import { List as TenantList } from '@/api/imTenant';
|
import { List as TenantList } from '@/api/imTenant';
|
||||||
|
import { Status } from '@/api/imUser';
|
||||||
|
import { usePermission } from '@/hooks/web/usePermission';
|
||||||
|
|
||||||
const dict = useDictStore();
|
const dict = useDictStore();
|
||||||
|
const { hasPermission } = usePermission();
|
||||||
|
const $message = window['$message'];
|
||||||
|
|
||||||
export const tenantOptions = ref<any[]>([]);
|
export const tenantOptions = ref<any[]>([]);
|
||||||
|
|
||||||
@@ -43,6 +48,21 @@ export class State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DeviceState {
|
||||||
|
public tenantId = 0;
|
||||||
|
public tenantName = '';
|
||||||
|
public userId = '';
|
||||||
|
public deviceId = '';
|
||||||
|
public platform = '';
|
||||||
|
public lang = '';
|
||||||
|
public lastSeen = '';
|
||||||
|
public pushProvider = '';
|
||||||
|
public pushPlatform = '';
|
||||||
|
public pushAppVersion = '';
|
||||||
|
public pushTokenConfigured = false;
|
||||||
|
public pushUpdatedAt = '';
|
||||||
|
}
|
||||||
|
|
||||||
export function newState(state: State | Record<string, any> | null): State {
|
export function newState(state: State | Record<string, any> | null): State {
|
||||||
if (state !== null) {
|
if (state !== null) {
|
||||||
if (state instanceof State) {
|
if (state instanceof State) {
|
||||||
@@ -159,7 +179,25 @@ export const columns = [
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
width: 110,
|
width: 110,
|
||||||
render(row: State) {
|
render(row: State) {
|
||||||
return dict.getLabel('ImUserStateOptions', row.state);
|
if (row.state === 20) {
|
||||||
|
return dict.getLabel('ImUserStateOptions', row.state);
|
||||||
|
}
|
||||||
|
return h(NSwitch, {
|
||||||
|
value: row.state === 0,
|
||||||
|
checked: '正常',
|
||||||
|
unchecked: '停用',
|
||||||
|
disabled: !hasPermission(['/imUser/status']),
|
||||||
|
onUpdateValue: async (value) => {
|
||||||
|
const oldState = row.state;
|
||||||
|
row.state = value ? 0 : 10;
|
||||||
|
try {
|
||||||
|
await Status({ tenantId: row.tenantId, userId: row.userId, state: row.state });
|
||||||
|
$message.success('操作成功');
|
||||||
|
} catch {
|
||||||
|
row.state = oldState;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -200,6 +238,26 @@ export const columns = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const deviceColumns = [
|
||||||
|
{ title: '设备ID', key: 'deviceId', align: 'left', width: 240 },
|
||||||
|
{ title: '平台', key: 'platform', align: 'left', width: 120 },
|
||||||
|
{ title: '语言', key: 'lang', align: 'left', width: 90 },
|
||||||
|
{ title: '最后活跃', key: 'lastSeen', align: 'left', width: 180 },
|
||||||
|
{ title: 'Push服务商', key: 'pushProvider', align: 'left', width: 120 },
|
||||||
|
{ title: 'Push平台', key: 'pushPlatform', align: 'left', width: 120 },
|
||||||
|
{ title: 'App版本', key: 'pushAppVersion', align: 'left', width: 120 },
|
||||||
|
{
|
||||||
|
title: 'Push Token',
|
||||||
|
key: 'pushTokenConfigured',
|
||||||
|
align: 'left',
|
||||||
|
width: 120,
|
||||||
|
render(row: DeviceState) {
|
||||||
|
return row.pushTokenConfigured ? '已配置' : '未配置';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: 'Push更新时间', key: 'pushUpdatedAt', align: 'left', width: 180 },
|
||||||
|
];
|
||||||
|
|
||||||
// 加载字典数据选项
|
// 加载字典数据选项
|
||||||
export function loadOptions() {
|
export function loadOptions() {
|
||||||
dict.loadOptions(['ImUserStateOptions']);
|
dict.loadOptions(['ImUserStateOptions']);
|
||||||
|
|||||||
Reference in New Issue
Block a user