mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
feat: 资源操作新增右键菜单操作等
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<el-form-item v-bind="$attrs">
|
||||
<template #label>
|
||||
{{ props.label }}
|
||||
<div class="flex items-center">
|
||||
{{ props.label }}
|
||||
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
@@ -24,14 +26,15 @@ import { useSlots } from 'vue';
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'top',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,38 +5,36 @@
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
||||
|
||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-row class="!w-full">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
:active-text="$t('common.enable')"
|
||||
:inactive-text="$t('common.disable')"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-row class="!w-full">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
:active-text="$t('common.enable')"
|
||||
:inactive-text="$t('common.disable')"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required>
|
||||
<el-radio-group v-model="form.cronAble">
|
||||
<el-radio :label="$t('common.yes')" :value="1" />
|
||||
<el-radio :label="$t('common.no')" :value="-1" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required label-position="left">
|
||||
<el-radio-group v-model="form.cronAble">
|
||||
<el-radio :label="$t('common.yes')" :value="1" />
|
||||
<el-radio :label="$t('common.no')" :value="-1" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||
<CrontabInput v-model="form.cron" />
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
||||
<el-row>
|
||||
<el-row gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||
@@ -22,7 +22,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-position="left" label-width="60" required>
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
@@ -59,7 +59,7 @@
|
||||
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row>
|
||||
<el-row gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
||||
<el-select v-model="form.targetTableName" filterable>
|
||||
@@ -80,7 +80,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-row gutter="10">
|
||||
<el-col :span="12">
|
||||
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
||||
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
||||
@@ -94,7 +94,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-row gutter="10">
|
||||
<el-col :span="12">
|
||||
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
||||
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
||||
@@ -105,17 +105,32 @@
|
||||
|
||||
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
||||
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight" size="small">
|
||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
|
||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight">
|
||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200"></el-table-column>
|
||||
<el-table-column prop="target" :label="$t('db.targetField')">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.target" allow-create filterable>
|
||||
<template #label="{ label, value }">
|
||||
<div class="flex justify-between">
|
||||
<el-text tag="b">{{ value }}</el-text>
|
||||
<el-text size="small">{{ label }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-option
|
||||
v-for="item in state.targetColumnList"
|
||||
:key="item.columnName"
|
||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
||||
:label="`${item.columnType}${item.columnComment && ' - ' + item.columnComment}`"
|
||||
:value="item.columnName"
|
||||
/>
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
{{ item.columnName }}
|
||||
|
||||
<el-text size="small">
|
||||
{{ item.columnType }}{{ item.columnComment && ' - ' + item.columnComment }}
|
||||
</el-text>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
|
||||
@@ -111,8 +111,6 @@
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<div style="text-align: center; margin-top: 10px"></div>
|
||||
|
||||
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
||||
<el-form-item prop="key" label="Key" required>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="treeNodeClick"
|
||||
@node-expand="treeNodeClick"
|
||||
@node-contextmenu="onNodeContextmenu"
|
||||
:default-expanded-keys="state.defaultExpandedKeys"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
@@ -65,12 +66,15 @@
|
||||
</el-card>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
|
||||
<Contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
@@ -108,6 +112,7 @@ const { t } = useI18n();
|
||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||
|
||||
const treeRef: any = useTemplateRef('treeRef');
|
||||
const contextmenuRef: any = useTemplateRef('contextmenuRef');
|
||||
|
||||
// 存储所有注册的资源组件引用,key -> 组件名称
|
||||
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
||||
@@ -134,6 +139,11 @@ const setResourceComponentRefs = async (name: string, ref: any) => {
|
||||
const state = reactive({
|
||||
defaultExpandedKeys: [] as string[],
|
||||
filterText: '',
|
||||
contextmenuItems: [],
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const { filterText } = toRefs(state);
|
||||
@@ -216,6 +226,9 @@ const loadNode = async (node: any, resolve: (data: any) => void, reject: () => v
|
||||
let lastNodeClickTime = 0;
|
||||
|
||||
const treeNodeClick = async (data: any, node: any) => {
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value?.closeContextmenu();
|
||||
|
||||
const currentClickNodeTime = Date.now();
|
||||
// 双击节点
|
||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
||||
@@ -248,6 +261,29 @@ const treeNodeDblclick = async (data: any, node: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const onNodeContextmenu = (event: any, data: any) => {
|
||||
if (data.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
if (props.loadContextmenuItems) {
|
||||
items = props.loadContextmenuItems(data);
|
||||
}
|
||||
}
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
state.contextmenuItems = items;
|
||||
const { clientX, clientY } = event;
|
||||
state.dropdown.x = clientX;
|
||||
state.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
// 初始化资源组件ref
|
||||
const initResourceComp = (val: any) => {
|
||||
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
||||
|
||||
@@ -6,11 +6,11 @@ require (
|
||||
gitee.com/chunanyong/dm v1.8.20
|
||||
gitee.com/liuzongyang/libpq v1.10.11
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
@@ -26,11 +26,11 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/redis/go-redis/v9 v9.13.0
|
||||
github.com/redis/go-redis/v9 v9.14.0
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/spf13/cast v1.9.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.30.5
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -69,7 +69,6 @@ require (
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
|
||||
@@ -94,7 +94,7 @@ func (app *dataSyncAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
|
||||
func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
||||
if app.IsRunning(id) {
|
||||
logx.Warn("[%d] the db sync task is running...", id)
|
||||
logx.Warnf("[%d] the db sync task is running...", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
||||
logx.ErrorfContext(ctx, "data source connection unavailable: %s", err.Error())
|
||||
return
|
||||
}
|
||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
||||
srcConn, err := app.dbApp.GetDbConn(context.Background(), uint64(task.SrcDbId), task.SrcDbName)
|
||||
if err != nil {
|
||||
logx.ErrorfContext(ctx, "failed to connect to the source database: %s", err.Error())
|
||||
return
|
||||
@@ -381,6 +381,7 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
||||
|
||||
if err := app.CursorByCond(&entity.DataSyncTaskQuery{Status: entity.DataSyncTaskStatusEnable}, func(dst *entity.DataSyncTask) error {
|
||||
app.addCronJob(contextx.NewTraceId(), dst)
|
||||
app.MarkStop(dst.Id)
|
||||
return nil
|
||||
}); err != nil {
|
||||
logx.ErrorTrace("the db data sync task failed to initialize: %v", err)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -111,6 +112,12 @@ func ClearNumScale(column *Column) {
|
||||
column.CharMaxLength = 0
|
||||
}
|
||||
|
||||
func ClearNumPrecision(column *Column) {
|
||||
column.NumScale = 0
|
||||
column.NumPrecision = 0
|
||||
column.CharMaxLength = 0
|
||||
}
|
||||
|
||||
// DataType 数据类型, 对应于go类型,如int int64等。可自定义其他类型
|
||||
type DataType struct {
|
||||
Name string // 类型名
|
||||
@@ -173,7 +180,13 @@ func SQLValueString(val any) string {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("'%s'", strings.ReplaceAll(strings.ReplaceAll(strVal, "'", "''"), `\`, `\\`))
|
||||
// 使用 strconv.Quote 来处理所有特殊字符
|
||||
quoted := strconv.Quote(strVal)
|
||||
// 去掉 strconv.Quote 添加的外层引号,因为会在最后添加 SQL 的单引号
|
||||
quoted = quoted[1 : len(quoted)-1]
|
||||
// 处理 SQL 中的单引号
|
||||
quoted = strings.ReplaceAll(quoted, "'", "''")
|
||||
return fmt.Sprintf("'%s'", quoted)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -24,17 +24,16 @@ type Meta struct {
|
||||
func (dm *Meta) GetSqlDb(ctx context.Context, d *dbi.DbInfo) (*sql.DB, error) {
|
||||
driverName := "dm"
|
||||
db := d.Database
|
||||
var dbParam string
|
||||
dbParam := "?escapeProcess=true"
|
||||
if db != "" {
|
||||
// dm database可以使用db/schema表示,方便连接指定schema, 若不存在schema则使用默认schema
|
||||
ss := strings.Split(db, "/")
|
||||
if len(ss) > 1 {
|
||||
dbParam = fmt.Sprintf("%s?schema=\"%s\"&escapeProcess=true", ss[0], ss[len(ss)-1])
|
||||
} else {
|
||||
dbParam = db + "?escapeProcess=true"
|
||||
dbParam = fmt.Sprintf("%s&schema=\"%s\"", dbParam, ss[len(ss)-1])
|
||||
}
|
||||
} else {
|
||||
dbParam = "?escapeProcess=true"
|
||||
}
|
||||
if d.Params != "" {
|
||||
dbParam += "&" + d.Params
|
||||
}
|
||||
|
||||
err := d.IfUseSshTunnelChangeIpPort(ctx)
|
||||
|
||||
@@ -22,7 +22,7 @@ var (
|
||||
UnsignedMediumint = dbi.NewDbDataType("unsigned mediumint", dbi.DTInt64).WithCT(dbi.CTUnsignedInt4).WithFixColumn(dbi.ClearNumScale)
|
||||
|
||||
Decimal = dbi.NewDbDataType("decimal", dbi.DTDecimal).WithCT(dbi.CTDecimal)
|
||||
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
||||
Double = dbi.NewDbDataType("double", dbi.DTNumeric).WithCT(dbi.CTNumeric).WithFixColumn(dbi.ClearNumPrecision)
|
||||
Float = dbi.NewDbDataType("float", dbi.DTNumeric).WithCT(dbi.CTNumeric)
|
||||
|
||||
Varchar = dbi.NewDbDataType("varchar", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||
@@ -40,9 +40,9 @@ var (
|
||||
Enum = dbi.NewDbDataType("enum", dbi.DTString).WithCT(dbi.CTEnum)
|
||||
Set = dbi.NewDbDataType("set", dbi.DTString).WithCT(dbi.CTVarchar)
|
||||
|
||||
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob)
|
||||
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob)
|
||||
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob)
|
||||
Blob = dbi.NewDbDataType("blob", dbi.DTBytes).WithCT(dbi.CTBlob).WithFixColumn(dbi.ClearNumScale)
|
||||
Mediumblob = dbi.NewDbDataType("mediumblob", dbi.DTBytes).WithCT(dbi.CTMediumblob).WithFixColumn(dbi.ClearNumScale)
|
||||
Longblob = dbi.NewDbDataType("longblob", dbi.DTBytes).WithCT(dbi.CTLongblob).WithFixColumn(dbi.ClearNumScale)
|
||||
Binary = dbi.NewDbDataType("binary", dbi.DTBytes).WithCT(dbi.CTBinary)
|
||||
Varbinary = dbi.NewDbDataType("varbinary", dbi.DTBytes).WithCT(dbi.CTVarbinary)
|
||||
)
|
||||
|
||||
@@ -30,17 +30,16 @@ func (ucs UserClients) Count() int {
|
||||
|
||||
// 连接管理
|
||||
type ClientManager struct {
|
||||
UserClientsMap map[UserId]UserClients // 全部的用户连接, key->userid, value->UserClients
|
||||
RwLock sync.RWMutex // 读写锁
|
||||
UserClientsMap sync.Map // 全部的用户连接, key->userid, value->UserClients
|
||||
|
||||
ConnectChan chan *Client // 连接处理
|
||||
DisConnectChan chan *Client // 断开连接处理
|
||||
MsgChan chan *Msg // 消息信息channel通道
|
||||
MsgChan chan *Msg // 消息信息channel通道
|
||||
}
|
||||
|
||||
func NewClientManager() (clientManager *ClientManager) {
|
||||
return &ClientManager{
|
||||
UserClientsMap: make(map[UserId]UserClients),
|
||||
UserClientsMap: sync.Map{},
|
||||
ConnectChan: make(chan *Client, 10),
|
||||
DisConnectChan: make(chan *Client, 10),
|
||||
MsgChan: make(chan *Msg, 100),
|
||||
@@ -78,24 +77,30 @@ func (manager *ClientManager) CloseClient(client *Client) {
|
||||
|
||||
// 根据用户id关闭客户端连接
|
||||
func (manager *ClientManager) CloseByUid(userId UserId) {
|
||||
for _, client := range manager.GetByUid(userId) {
|
||||
userClients := manager.GetByUid(userId)
|
||||
for _, client := range userClients {
|
||||
manager.CloseClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有的客户端
|
||||
func (manager *ClientManager) AllUserClient() map[UserId]UserClients {
|
||||
manager.RwLock.RLock()
|
||||
defer manager.RwLock.RUnlock()
|
||||
|
||||
return manager.UserClientsMap
|
||||
result := make(map[UserId]UserClients)
|
||||
manager.UserClientsMap.Range(func(key, value any) bool {
|
||||
userId := key.(UserId)
|
||||
userClients := value.(UserClients)
|
||||
result[userId] = userClients
|
||||
return true
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// 通过userId获取用户所有客户端信息
|
||||
func (manager *ClientManager) GetByUid(userId UserId) UserClients {
|
||||
manager.RwLock.RLock()
|
||||
defer manager.RwLock.RUnlock()
|
||||
return manager.UserClientsMap[userId]
|
||||
if value, ok := manager.UserClientsMap.Load(userId); ok {
|
||||
return value.(UserClients)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 通过userId和clientId获取客户端信息
|
||||
@@ -108,9 +113,12 @@ func (manager *ClientManager) GetByUidAndCid(uid UserId, clientId string) *Clien
|
||||
|
||||
// 客户端数量
|
||||
func (manager *ClientManager) Count() int {
|
||||
manager.RwLock.RLock()
|
||||
defer manager.RwLock.RUnlock()
|
||||
return len(manager.UserClientsMap)
|
||||
count := 0
|
||||
manager.UserClientsMap.Range(func(key, value any) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
// 发送json数据给指定用户
|
||||
@@ -139,7 +147,8 @@ func (manager *ClientManager) WriteMessage() {
|
||||
}
|
||||
|
||||
// cid为空,则向该用户所有客户端发送该消息
|
||||
for _, cli := range manager.GetByUid(uid) {
|
||||
userClients := manager.GetByUid(uid)
|
||||
for _, cli := range userClients {
|
||||
if err := cli.WriteMsg(msg); err != nil {
|
||||
logx.Warnf("ws send message failed - [uid=%d, cid=%s]: %s", uid, cli.ClientId, err.Error())
|
||||
}
|
||||
@@ -156,21 +165,23 @@ func (manager *ClientManager) HeartbeatTimer() {
|
||||
for {
|
||||
<-ticker.C
|
||||
//发送心跳
|
||||
for userId, clis := range manager.AllUserClient() {
|
||||
manager.UserClientsMap.Range(func(key, value any) bool {
|
||||
userId := key.(UserId)
|
||||
clis := value.(UserClients)
|
||||
for _, cli := range clis {
|
||||
if cli == nil || cli.WsConn == nil {
|
||||
continue
|
||||
}
|
||||
if err := cli.Ping(); err != nil {
|
||||
manager.CloseClient(cli)
|
||||
logx.Debugf("WS - failed to send heartbeat: uid=%v, cid=%s, usercount=%d", userId, cli.ClientId, Manager.Count())
|
||||
logx.Debugf("WS - failed to send heartbeat: uid=%v, cid=%s, usercount=%d", userId, cli.ClientId, manager.Count())
|
||||
} else {
|
||||
logx.Debugf("WS - send heartbeat successfully: uid=%v, cid=%s", userId, cli.ClientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -192,31 +203,29 @@ func (manager *ClientManager) doDisconnect(client *Client) {
|
||||
client.WsConn = nil
|
||||
}
|
||||
manager.delUserClient4Map(client)
|
||||
logx.Debugf("WS client disconnected: uid=%d, cid=%s, usercount=%d", client.UserId, client.ClientId, Manager.Count())
|
||||
logx.Debugf("WS client disconnected: uid=%d, cid=%s, usercount=%d", client.UserId, client.ClientId, manager.Count())
|
||||
}
|
||||
|
||||
func (manager *ClientManager) addUserClient2Map(client *Client) {
|
||||
manager.RwLock.Lock()
|
||||
defer manager.RwLock.Unlock()
|
||||
|
||||
userClients := manager.UserClientsMap[client.UserId]
|
||||
if userClients == nil {
|
||||
userClients = make(UserClients)
|
||||
manager.UserClientsMap[client.UserId] = userClients
|
||||
// 先尝试加载现有的UserClients
|
||||
if value, ok := manager.UserClientsMap.Load(client.UserId); ok {
|
||||
userClients := value.(UserClients)
|
||||
userClients.AddClient(client)
|
||||
} else {
|
||||
// 创建新的UserClients
|
||||
userClients := make(UserClients)
|
||||
userClients.AddClient(client)
|
||||
manager.UserClientsMap.Store(client.UserId, userClients)
|
||||
}
|
||||
userClients.AddClient(client)
|
||||
}
|
||||
|
||||
func (manager *ClientManager) delUserClient4Map(client *Client) {
|
||||
manager.RwLock.Lock()
|
||||
defer manager.RwLock.Unlock()
|
||||
|
||||
userClients := manager.UserClientsMap[client.UserId]
|
||||
if userClients != nil {
|
||||
if value, ok := manager.UserClientsMap.Load(client.UserId); ok {
|
||||
userClients := value.(UserClients)
|
||||
userClients.DeleteByCid(client.ClientId)
|
||||
// 如果用户所有客户端都关闭,则移除manager中的UserClientsMap值
|
||||
if userClients.Count() == 0 {
|
||||
delete(manager.UserClientsMap, client.UserId)
|
||||
manager.UserClientsMap.Delete(client.UserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user